• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

javascript-obfuscator / javascript-obfuscator / 21638611094

03 Feb 2026 04:23PM UTC coverage: 96.273% (-0.4%) from 96.658%
21638611094

push

github

sanex3339
Set version 5.3.0 to beta

1924 of 2096 branches covered (91.79%)

Branch coverage included in aggregate %.

5955 of 6088 relevant lines covered (97.82%)

30517016.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.86
/src/pro-api/ProApiClient.ts
1
import { TInputOptions } from '../types/options/TInputOptions';
2
import {
3
    IProApiConfig,
4
    IProApiStreamMessage,
5
    IProObfuscationResult,
6
    TProApiProgressCallback
7
} from '../interfaces/pro-api/IProApiClient';
8
import { ApiError } from './ApiError';
6✔
9
import { ProApiObfuscationResult } from './ProApiObfuscationResult';
6✔
10

11
/**
12
 * Pro API Client
13
 * Handles communication with the obfuscator.io Pro API using streaming mode
14
 */
15
export class ProApiClient {
6✔
16
    /**
17
     * API host (can be overridden via OBFUSCATOR_API_HOST env var)
18
     */
19
    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
20
    private static readonly apiHost = process.env.OBFUSCATOR_API_HOST || 'https://obfuscator.io';
6✔
21

22
    private static readonly apiUrl = `${ProApiClient.apiHost}/api/v1/obfuscate`;
6✔
23

24
    private static readonly uploadTokenUrl = `${ProApiClient.apiHost}/api/v1/upload/token`;
6✔
25

26
    /**
27
     * Default timeout (5 minutes)
28
     */
29
    private static readonly defaultTimeout = 300000;
6✔
30

31
    /**
32
     * Threshold for using blob upload (4.4MB)
33
     * Vercel has a ~4.5MB body limit
34
     */
35
    private static readonly blobUploadThreshold = 4.4 * 1024 * 1024;
6✔
36

37
    private readonly config: {
38
        apiToken: string;
39
        timeout: number;
40
        version?: string;
41
    };
42

43
    public constructor(config: IProApiConfig) {
44
        this.config = {
228✔
45
            apiToken: config.apiToken,
46
            timeout: config.timeout ?? ProApiClient.defaultTimeout,
684✔
47
            version: config.version
48
        };
49
    }
50

51
    /**
52
     * Check if any Pro features are enabled in the options.
53
     * Pro features require the Pro API for cloud-based obfuscation.
54
     */
55
    public static hasProFeatures(options: TInputOptions): boolean {
56
        return options.vmObfuscation === true || options.parseHtml === true;
270✔
57
    }
58

59
    /**
60
     * Obfuscate code using the Pro API (streaming mode)
61
     * @param sourceCode - Source code to obfuscate
62
     * @param options - Obfuscation options
63
     * @param onProgress - Optional progress callback
64
     * @returns Promise resolving to obfuscation result
65
     */
66
    public async obfuscate(
67
        sourceCode: string,
68
        options: TInputOptions = {},
×
69
        onProgress?: TProApiProgressCallback
70
    ): Promise<IProObfuscationResult> {
71
        if (!ProApiClient.hasProFeatures(options)) {
216✔
72
            throw new ApiError('Obfuscator.io Pro obfuscation works only when Pro features set.', 400);
24✔
73
        }
74

75
        const requestBody = JSON.stringify({
192✔
76
            code: sourceCode,
77
            options
78
        });
79

80
        const bodySize = Buffer.byteLength(requestBody, 'utf8');
192✔
81

82
        if (bodySize > ProApiClient.blobUploadThreshold) {
192✔
83
            return this.obfuscateWithBlobUpload(requestBody, onProgress);
18✔
84
        }
85

86
        return this.obfuscateDirect(requestBody, onProgress);
174✔
87
    }
88

89
    /**
90
     * Direct obfuscation for small files
91
     */
92
    private async obfuscateDirect(
93
        requestBody: string,
94
        onProgress?: TProApiProgressCallback
95
    ): Promise<IProObfuscationResult> {
96
        const headers: Record<string, string> = {
174✔
97
            // eslint-disable-next-line @typescript-eslint/naming-convention
98
            'Content-Type': 'application/json',
99
            // eslint-disable-next-line @typescript-eslint/naming-convention
100
            'Accept': 'application/x-ndjson',
101
            // eslint-disable-next-line @typescript-eslint/naming-convention
102
            'Authorization': `Bearer ${this.config.apiToken}`
103
        };
104

105
        const controller = new AbortController();
174✔
106
        const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
174✔
107

108
        let url = ProApiClient.apiUrl;
174✔
109

110
        if (this.config.version) {
174✔
111
            url = `${ProApiClient.apiUrl}?version=${encodeURIComponent(this.config.version)}`;
54✔
112
        }
113

114
        try {
174✔
115
            const response = await fetch(url, {
174✔
116
                method: 'POST',
117
                headers,
118
                body: requestBody,
119
                signal: controller.signal
120
            });
121

122
            clearTimeout(timeoutId);
168✔
123

124
            return this.handleStreamingResponse(response, onProgress);
168✔
125
        } catch (error) {
126
            clearTimeout(timeoutId);
6✔
127

128
            if (error instanceof Error && error.name === 'AbortError') {
6✔
129
                throw new ApiError('Request timeout', 408);
6✔
130
            }
131

132
            throw error;
×
133
        }
134
    }
135

136
    /**
137
     * Obfuscation with blob upload for large files
138
     */
139
    private async obfuscateWithBlobUpload(
140
        requestBody: string,
141
        onProgress?: TProApiProgressCallback
142
    ): Promise<IProObfuscationResult> {
143
        onProgress?.('Uploading large file...');
18!
144

145
        const blobUrl = await this.uploadToBlob(requestBody);
18✔
146

147
        onProgress?.('File uploaded, starting obfuscation...');
×
148

149
        const headers: Record<string, string> = {
×
150
            // eslint-disable-next-line @typescript-eslint/naming-convention
151
            'Content-Type': 'application/json',
152
            // eslint-disable-next-line @typescript-eslint/naming-convention
153
            'Accept': 'application/x-ndjson',
154
            // eslint-disable-next-line @typescript-eslint/naming-convention
155
            'Authorization': `Bearer ${this.config.apiToken}`
156
        };
157

158
        const controller = new AbortController();
×
159
        const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
×
160

161
        let url = ProApiClient.apiUrl;
×
162

163
        if (this.config.version) {
×
164
            url = `${ProApiClient.apiUrl}?version=${encodeURIComponent(this.config.version)}`;
×
165
        }
166

167
        try {
×
168
            const response = await fetch(url, {
×
169
                method: 'POST',
170
                headers,
171
                body: JSON.stringify({ blobUrl }),
172
                signal: controller.signal
173
            });
174

175
            clearTimeout(timeoutId);
×
176

177
            return this.handleStreamingResponse(response, onProgress);
×
178
        } catch (error) {
179
            clearTimeout(timeoutId);
×
180

181
            if (error instanceof Error && error.name === 'AbortError') {
×
182
                throw new ApiError('Request timeout', 408);
×
183
            }
184

185
            throw error;
×
186
        }
187
    }
188

189
    /**
190
     * Upload request body to blob storage using client-side upload
191
     */
192
    private async uploadToBlob(requestBody: string): Promise<string> {
193
        const pathname = 'obfuscate-request.json';
18✔
194

195
        // Step 1: Get client upload token from server (server adds random suffix)
196
        const clientToken = await this.getUploadToken(pathname);
18✔
197

198
        // Step 2: Upload directly to Vercel Blob using the client token
199
        return this.uploadWithClientToken(clientToken, pathname, requestBody);
6✔
200
    }
201

202
    /**
203
     * Get a client upload token from the server
204
     */
205
    private async getUploadToken(pathname: string): Promise<string> {
206
        const controller = new AbortController();
18✔
207
        const timeoutId = setTimeout(() => controller.abort(), 30000);
18✔
208

209
        try {
18✔
210
            const response = await fetch(ProApiClient.uploadTokenUrl, {
18✔
211
                method: 'POST',
212
                headers: {
213
                    // eslint-disable-next-line @typescript-eslint/naming-convention
214
                    'Content-Type': 'application/json',
215
                    // eslint-disable-next-line @typescript-eslint/naming-convention
216
                    'Authorization': `Bearer ${this.config.apiToken}`
217
                },
218
                body: JSON.stringify({ pathname }),
219
                signal: controller.signal
220
            });
221

222
            clearTimeout(timeoutId);
18✔
223

224
            const responseText = await response.text();
18✔
225

226
            let data: { clientToken?: string; error?: string };
227

228
            try {
18✔
229
                data = JSON.parse(responseText);
18✔
230
            } catch {
231
                throw new ApiError(responseText || 'Failed to get upload token', response.status);
×
232
            }
233

234
            if (!response.ok) {
18✔
235
                throw new ApiError(data.error ?? 'Failed to get upload token', response.status);
6!
236
            }
237

238
            if (!data.clientToken) {
12✔
239
                throw new ApiError('No client token returned', 500);
6✔
240
            }
241

242
            return data.clientToken;
6✔
243
        } catch (error) {
244
            clearTimeout(timeoutId);
12✔
245

246
            if (error instanceof ApiError) {
12✔
247
                throw error;
12✔
248
            }
249

250
            if (error instanceof Error && error.name === 'AbortError') {
×
251
                throw new ApiError('Token request timeout', 408);
×
252
            }
253

254
            throw error;
×
255
        }
256
    }
257

258
    /**
259
     * Upload file directly to Vercel Blob using client token
260
     */
261
    private async uploadWithClientToken(clientToken: string, pathname: string, requestBody: string): Promise<string> {
262
        const blobClient = await import('@vercel/blob/client');
12✔
263

264
        const controller = new AbortController();
6✔
265
        const timeoutId = setTimeout(() => controller.abort(), 120000); // 2 minutes for upload
6✔
266

267
        try {
6✔
268
            const blob = await blobClient.put(pathname, requestBody, {
6✔
269
                access: 'public',
270
                token: clientToken
271
            });
272

273
            clearTimeout(timeoutId);
×
274

275
            return blob.url;
×
276
        } catch (error) {
277
            clearTimeout(timeoutId);
6✔
278

279
            if (error instanceof ApiError) {
6!
280
                throw error;
×
281
            }
282

283
            if (error instanceof Error && error.name === 'AbortError') {
6!
284
                throw new ApiError('Upload timeout', 408);
×
285
            }
286

287
            if (error instanceof Error) {
6✔
288
                throw new ApiError(`Upload failed: ${error.message}`, 500);
6✔
289
            }
290

291
            throw error;
×
292
        }
293
    }
294

295
    /**
296
     * Handle streaming (NDJSON) response from API
297
     * Supports both direct result and chunked response formats
298
     */
299
    // eslint-disable-next-line complexity
300
    private async handleStreamingResponse(
301
        response: Response,
302
        onProgress?: TProApiProgressCallback
303
    ): Promise<IProObfuscationResult> {
304
        const text = await response.text();
168✔
305
        const lines = text.trim().split('\n');
168✔
306

307
        const messages: IProApiStreamMessage[] = [];
168✔
308

309
        for (const line of lines) {
168✔
310
            if (!line.trim()) {
312!
311
                continue;
×
312
            }
313

314
            try {
312✔
315
                const message: IProApiStreamMessage = JSON.parse(line);
312✔
316
                messages.push(message);
306✔
317

318
                if (message.type === 'progress' && message.message && onProgress) {
306✔
319
                    onProgress(message.message);
30✔
320
                }
321
            } catch {
322
                // Skip invalid JSON lines
323
            }
324
        }
325

326
        const errorMessage = messages.find((message) => message.type === 'error');
306✔
327

328
        if (errorMessage) {
168✔
329
            throw new ApiError(errorMessage.message ?? 'Unknown API error', response.status);
12!
330
        }
331

332
        const result = this.reassembleChunkedResponse(messages);
156✔
333

334
        if (!result.code) {
156✔
335
            throw new ApiError('No result received from API', 500);
6✔
336
        }
337

338
        return new ProApiObfuscationResult(result.code, result.sourceMap || '');
150✔
339
    }
340

341
    /**
342
     * Reassemble chunked streaming response
343
     * Handles both chunked format (chunk/chunk_end) and direct result format
344
     */
345
    // eslint-disable-next-line complexity
346
    private reassembleChunkedResponse(messages: IProApiStreamMessage[]): { code: string; sourceMap: string } {
347
        const codeChunks: string[] = [];
156✔
348
        const sourceMapChunks: string[] = [];
156✔
349
        let result = { code: '', sourceMap: '' };
156✔
350

351
        for (const message of messages) {
156✔
352
            switch (message.type) {
288✔
353
                case 'chunk':
222✔
354
                    if (message.field === 'code' && message.data !== undefined && message.index !== undefined) {
72✔
355
                        codeChunks[message.index] = message.data;
60✔
356
                    } else if (
12✔
357
                        message.field === 'sourceMap' &&
36✔
358
                        message.data !== undefined &&
359
                        message.index !== undefined
360
                    ) {
361
                        sourceMapChunks[message.index] = message.data;
12✔
362
                    }
363

364
                    break;
72✔
365

366
                case 'chunk_end':
367
                    result.code = codeChunks.join('');
24✔
368
                    result.sourceMap = (sourceMapChunks.join('') || message.sourceMap) ?? '';
24!
369

370
                    break;
24✔
371

372
                case 'result':
373
                    result = {
126✔
374
                        code: message.code ?? '',
378!
375
                        sourceMap: message.sourceMap ?? ''
378!
376
                    };
377

378
                    break;
126✔
379
            }
380
        }
381

382
        return result;
156✔
383
    }
384
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc