Skip to content

Commit fa0be01

Browse files
mikolalysenkoclaude
andcommitted
Add support for public patch API proxy
When SOCKET_API_TOKEN is not set, the CLI now automatically uses the public patch API proxy (patch-api.socket.dev) to access free patches without authentication. Changes: - APIClient now supports optional authentication for public proxy mode - getAPIClientFromEnv() returns { client, usePublicProxy } tuple - --org is now optional when using the public proxy - Updated examples to show no-auth usage first 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent d5c0ebf commit fa0be01

File tree

2 files changed

+103
-50
lines changed

2 files changed

+103
-50
lines changed

src/commands/download.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type IdentifierType = 'uuid' | 'cve' | 'ghsa' | 'package'
2424

2525
interface DownloadArgs {
2626
identifier: string
27-
org: string
27+
org?: string
2828
cwd: string
2929
id?: boolean
3030
cve?: boolean
@@ -179,8 +179,18 @@ async function downloadPatches(args: DownloadArgs): Promise<boolean> {
179179
process.env.SOCKET_API_TOKEN = apiToken
180180
}
181181

182-
// Get API client
183-
const apiClient = getAPIClientFromEnv()
182+
// Get API client (will use public proxy if no token is set)
183+
const { client: apiClient, usePublicProxy } = getAPIClientFromEnv()
184+
185+
// Validate that org is provided when using authenticated API
186+
if (!usePublicProxy && !orgSlug) {
187+
throw new Error(
188+
'--org is required when using SOCKET_API_TOKEN. Provide an organization slug.',
189+
)
190+
}
191+
192+
// The org slug to use (null when using public proxy)
193+
const effectiveOrgSlug = usePublicProxy ? null : orgSlug ?? null
184194

185195
// Determine identifier type
186196
let idType: IdentifierType
@@ -200,7 +210,7 @@ async function downloadPatches(args: DownloadArgs): Promise<boolean> {
200210
// For UUID, directly fetch and download the patch
201211
if (idType === 'uuid') {
202212
console.log(`Fetching patch by UUID: ${identifier}`)
203-
const patch = await apiClient.fetchPatch(orgSlug, identifier)
213+
const patch = await apiClient.fetchPatch(effectiveOrgSlug, identifier)
204214
if (!patch) {
205215
console.log(`No patch found with UUID: ${identifier}`)
206216
return true
@@ -246,18 +256,18 @@ async function downloadPatches(args: DownloadArgs): Promise<boolean> {
246256
switch (idType) {
247257
case 'cve': {
248258
console.log(`Searching patches for CVE: ${identifier}`)
249-
searchResponse = await apiClient.searchPatchesByCVE(orgSlug, identifier)
259+
searchResponse = await apiClient.searchPatchesByCVE(effectiveOrgSlug, identifier)
250260
break
251261
}
252262
case 'ghsa': {
253263
console.log(`Searching patches for GHSA: ${identifier}`)
254-
searchResponse = await apiClient.searchPatchesByGHSA(orgSlug, identifier)
264+
searchResponse = await apiClient.searchPatchesByGHSA(effectiveOrgSlug, identifier)
255265
break
256266
}
257267
case 'package': {
258268
console.log(`Searching patches for package: ${identifier}`)
259269
searchResponse = await apiClient.searchPatchesByPackage(
260-
orgSlug,
270+
effectiveOrgSlug,
261271
identifier,
262272
)
263273
break
@@ -331,7 +341,7 @@ async function downloadPatches(args: DownloadArgs): Promise<boolean> {
331341

332342
for (const searchResult of accessiblePatches) {
333343
// Fetch full patch details with blob content
334-
const patch = await apiClient.fetchPatch(orgSlug, searchResult.uuid)
344+
const patch = await apiClient.fetchPatch(effectiveOrgSlug, searchResult.uuid)
335345
if (!patch) {
336346
console.log(` [fail] ${searchResult.purl} (could not fetch details)`)
337347
patchesFailed++
@@ -383,9 +393,9 @@ export const downloadCommand: CommandModule<{}, DownloadArgs> = {
383393
demandOption: true,
384394
})
385395
.option('org', {
386-
describe: 'Organization slug',
396+
describe: 'Organization slug (required when using SOCKET_API_TOKEN, optional for public proxy)',
387397
type: 'string',
388-
demandOption: true,
398+
demandOption: false,
389399
})
390400
.option('id', {
391401
describe: 'Force identifier to be treated as a patch UUID',
@@ -427,24 +437,24 @@ export const downloadCommand: CommandModule<{}, DownloadArgs> = {
427437
type: 'string',
428438
})
429439
.example(
430-
'$0 download 12345678-1234-1234-1234-123456789abc --org myorg',
431-
'Download a patch by UUID',
440+
'$0 download CVE-2021-44228',
441+
'Download free patches for a CVE (no auth required)',
432442
)
433443
.example(
434-
'$0 download CVE-2021-44228 --org myorg',
435-
'Search and download patches for a CVE',
444+
'$0 download GHSA-jfhm-5ghh-2f97',
445+
'Download free patches for a GHSA (no auth required)',
436446
)
437447
.example(
438-
'$0 download GHSA-jfhm-5ghh-2f97 --org myorg',
439-
'Search and download patches for a GHSA',
448+
'$0 download lodash --pkg',
449+
'Download free patches for a package (no auth required)',
440450
)
441451
.example(
442-
'$0 download lodash --org myorg --pkg',
443-
'Search and download patches for a package',
452+
'$0 download 12345678-1234-1234-1234-123456789abc --org myorg',
453+
'Download a patch by UUID (requires SOCKET_API_TOKEN)',
444454
)
445455
.example(
446456
'$0 download CVE-2021-44228 --org myorg --yes',
447-
'Download all matching patches without confirmation',
457+
'Download all matching patches without confirmation (with auth)',
448458
)
449459
.check(argv => {
450460
// Ensure only one type flag is set

src/utils/api-client.ts

Lines changed: 74 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as https from 'node:https'
22
import * as http from 'node:http'
33

4+
// Default public patch API proxy URL for free patches (no auth required)
5+
const DEFAULT_PATCH_API_PROXY_URL = 'https://patch-api.socket.dev'
6+
47
// Full patch response with blob content (from view endpoint)
58
export interface PatchResponse {
69
uuid: string
@@ -55,16 +58,23 @@ export interface SearchResponse {
5558

5659
export interface APIClientOptions {
5760
apiUrl: string
58-
apiToken: string
61+
apiToken?: string
62+
/**
63+
* When true, the client will use the public patch API proxy
64+
* which only provides access to free patches without authentication.
65+
*/
66+
usePublicProxy?: boolean
5967
}
6068

6169
export class APIClient {
6270
private readonly apiUrl: string
63-
private readonly apiToken: string
71+
private readonly apiToken?: string
72+
private readonly usePublicProxy: boolean
6473

6574
constructor(options: APIClientOptions) {
6675
this.apiUrl = options.apiUrl.replace(/\/$/, '') // Remove trailing slash
6776
this.apiToken = options.apiToken
77+
this.usePublicProxy = options.usePublicProxy ?? false
6878
}
6979

7080
/**
@@ -78,12 +88,18 @@ export class APIClient {
7888
const isHttps = urlObj.protocol === 'https:'
7989
const httpModule = isHttps ? https : http
8090

91+
const headers: Record<string, string> = {
92+
Accept: 'application/json',
93+
}
94+
95+
// Only add auth header if we have a token (not using public proxy)
96+
if (this.apiToken) {
97+
headers['Authorization'] = `Bearer ${this.apiToken}`
98+
}
99+
81100
const options: https.RequestOptions = {
82101
method: 'GET',
83-
headers: {
84-
Authorization: `Bearer ${this.apiToken}`,
85-
Accept: 'application/json',
86-
},
102+
headers,
87103
}
88104

89105
const req = httpModule.request(urlObj, options, res => {
@@ -106,11 +122,10 @@ export class APIClient {
106122
} else if (res.statusCode === 401) {
107123
reject(new Error('Unauthorized: Invalid API token'))
108124
} else if (res.statusCode === 403) {
109-
reject(
110-
new Error(
111-
'Forbidden: Access denied. This may be a paid patch or you may not have access to this organization.',
112-
),
113-
)
125+
const msg = this.usePublicProxy
126+
? 'Forbidden: This patch is only available to paid subscribers. Sign up at https://socket.dev to access paid patches.'
127+
: 'Forbidden: Access denied. This may be a paid patch or you may not have access to this organization.'
128+
reject(new Error(msg))
114129
} else if (res.statusCode === 429) {
115130
reject(new Error('Rate limit exceeded. Please try again later.'))
116131
} else {
@@ -135,23 +150,29 @@ export class APIClient {
135150
* Fetch a patch by UUID (full details with blob content)
136151
*/
137152
async fetchPatch(
138-
orgSlug: string,
153+
orgSlug: string | null,
139154
uuid: string,
140155
): Promise<PatchResponse | null> {
141-
return this.get(`/v0/orgs/${orgSlug}/patches/view/${uuid}`)
156+
// Public proxy uses simpler URL structure (no org slug needed)
157+
const path = this.usePublicProxy
158+
? `/view/${uuid}`
159+
: `/v0/orgs/${orgSlug}/patches/view/${uuid}`
160+
return this.get(path)
142161
}
143162

144163
/**
145164
* Search patches by CVE ID
146165
* Returns lightweight search results (no blob content)
147166
*/
148167
async searchPatchesByCVE(
149-
orgSlug: string,
168+
orgSlug: string | null,
150169
cveId: string,
151170
): Promise<SearchResponse> {
152-
const result = await this.get<SearchResponse>(
153-
`/v0/orgs/${orgSlug}/patches/by-cve/${encodeURIComponent(cveId)}`,
154-
)
171+
// Public proxy uses simpler URL structure (no org slug needed)
172+
const path = this.usePublicProxy
173+
? `/by-cve/${encodeURIComponent(cveId)}`
174+
: `/v0/orgs/${orgSlug}/patches/by-cve/${encodeURIComponent(cveId)}`
175+
const result = await this.get<SearchResponse>(path)
155176
return result ?? { patches: [], canAccessPaidPatches: false }
156177
}
157178

@@ -160,12 +181,14 @@ export class APIClient {
160181
* Returns lightweight search results (no blob content)
161182
*/
162183
async searchPatchesByGHSA(
163-
orgSlug: string,
184+
orgSlug: string | null,
164185
ghsaId: string,
165186
): Promise<SearchResponse> {
166-
const result = await this.get<SearchResponse>(
167-
`/v0/orgs/${orgSlug}/patches/by-ghsa/${encodeURIComponent(ghsaId)}`,
168-
)
187+
// Public proxy uses simpler URL structure (no org slug needed)
188+
const path = this.usePublicProxy
189+
? `/by-ghsa/${encodeURIComponent(ghsaId)}`
190+
: `/v0/orgs/${orgSlug}/patches/by-ghsa/${encodeURIComponent(ghsaId)}`
191+
const result = await this.get<SearchResponse>(path)
169192
return result ?? { patches: [], canAccessPaidPatches: false }
170193
}
171194

@@ -174,25 +197,45 @@ export class APIClient {
174197
* Returns lightweight search results (no blob content)
175198
*/
176199
async searchPatchesByPackage(
177-
orgSlug: string,
200+
orgSlug: string | null,
178201
packageQuery: string,
179202
): Promise<SearchResponse> {
180-
const result = await this.get<SearchResponse>(
181-
`/v0/orgs/${orgSlug}/patches/by-package/${encodeURIComponent(packageQuery)}`,
182-
)
203+
// Public proxy uses simpler URL structure (no org slug needed)
204+
const path = this.usePublicProxy
205+
? `/by-package/${encodeURIComponent(packageQuery)}`
206+
: `/v0/orgs/${orgSlug}/patches/by-package/${encodeURIComponent(packageQuery)}`
207+
const result = await this.get<SearchResponse>(path)
183208
return result ?? { patches: [], canAccessPaidPatches: false }
184209
}
185210
}
186211

187-
export function getAPIClientFromEnv(): APIClient {
188-
const apiUrl = process.env.SOCKET_API_URL || 'https://api.socket.dev'
212+
/**
213+
* Get an API client configured from environment variables.
214+
*
215+
* If SOCKET_API_TOKEN is not set, the client will use the public patch API proxy
216+
* which provides free access to free-tier patches without authentication.
217+
*
218+
* Environment variables:
219+
* - SOCKET_API_URL: Override the API URL (defaults to https://api.socket.dev)
220+
* - SOCKET_API_TOKEN: API token for authenticated access to all patches
221+
* - SOCKET_PATCH_PROXY_URL: Override the public proxy URL (defaults to https://patch-api.socket.dev)
222+
*/
223+
export function getAPIClientFromEnv(): { client: APIClient; usePublicProxy: boolean } {
189224
const apiToken = process.env.SOCKET_API_TOKEN
190225

191226
if (!apiToken) {
192-
throw new Error(
193-
'SOCKET_API_TOKEN environment variable is required. Please set it to your Socket API token.',
194-
)
227+
// No token provided - use public proxy for free patches
228+
const proxyUrl = process.env.SOCKET_PATCH_PROXY_URL || DEFAULT_PATCH_API_PROXY_URL
229+
console.log('No SOCKET_API_TOKEN set. Using public patch API proxy (free patches only).')
230+
return {
231+
client: new APIClient({ apiUrl: proxyUrl, usePublicProxy: true }),
232+
usePublicProxy: true,
233+
}
195234
}
196235

197-
return new APIClient({ apiUrl, apiToken })
236+
const apiUrl = process.env.SOCKET_API_URL || 'https://api.socket.dev'
237+
return {
238+
client: new APIClient({ apiUrl, apiToken }),
239+
usePublicProxy: false,
240+
}
198241
}

0 commit comments

Comments
 (0)