11import * as https from 'node:https'
22import * 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)
58export interface PatchResponse {
69 uuid : string
@@ -55,16 +58,23 @@ export interface SearchResponse {
5558
5659export 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
6169export 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