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

javascript-obfuscator / javascript-obfuscator / 29668709393

17 Jul 2026 06:46PM UTC coverage: 95.938% (-0.02%) from 95.959%
29668709393

push

github

web-flow
Reworked large file uploads (#1438)

2021 of 2218 branches covered (91.12%)

Branch coverage included in aggregate %.

8 of 9 new or added lines in 1 file covered. (88.89%)

6128 of 6276 relevant lines covered (97.64%)

30272001.5 hits per line

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

75.24
/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. Matches the server's inline cap,
33
     * just under Vercel's 4.5MB (decimal) body limit.
34
     */
35
    private static readonly blobUploadThreshold = 4_400_000;
6✔
36

37
    /**
38
     * Headroom for the blob URL and JSON envelope in the follow-up request
39
     */
40
    private static readonly followUpBodyHeadroom = 512;
6✔
41

42
    private readonly config: {
43
        apiToken: string;
44
        timeout: number;
45
        version?: string;
46
    };
47

48
    public constructor(config: IProApiConfig) {
49
        this.config = {
240✔
50
            apiToken: config.apiToken,
51
            timeout: config.timeout ?? ProApiClient.defaultTimeout,
720✔
52
            version: config.version
53
        };
54
    }
55

56
    /**
57
     * Check if any Pro features are enabled in the options.
58
     * Pro features require the Pro API for cloud-based obfuscation.
59
     */
60
    public static hasProFeatures(options: TInputOptions): boolean {
61
        return options.vmObfuscation === true || options.parseHtml === true;
282✔
62
    }
63

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

80
        const requestBody = JSON.stringify({
204✔
81
            code: sourceCode,
82
            options
83
        });
84

85
        const bodySize = Buffer.byteLength(requestBody, 'utf8');
204✔
86

87
        if (bodySize > ProApiClient.blobUploadThreshold) {
204✔
88
            return this.obfuscateWithBlobUpload(sourceCode, options, requestBody, onProgress);
30✔
89
        }
90

91
        return this.obfuscateDirect(requestBody, onProgress);
174✔
92
    }
93

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

110
        const controller = new AbortController();
174✔
111
        const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
174✔
112

113
        let url = ProApiClient.apiUrl;
174✔
114

115
        if (this.config.version) {
174✔
116
            url = `${ProApiClient.apiUrl}?version=${encodeURIComponent(this.config.version)}`;
54✔
117
        }
118

119
        try {
174✔
120
            const response = await fetch(url, {
174✔
121
                method: 'POST',
122
                headers,
123
                body: requestBody,
124
                signal: controller.signal
125
            });
126

127
            clearTimeout(timeoutId);
168✔
128

129
            return this.handleStreamingResponse(response, onProgress);
168✔
130
        } catch (error) {
131
            clearTimeout(timeoutId);
6✔
132

133
            if (error instanceof Error && error.name === 'AbortError') {
6✔
134
                throw new ApiError('Request timeout', 408);
6✔
135
            }
136

137
            throw error;
×
138
        }
139
    }
140

141
    /**
142
     * Obfuscation with blob upload for large files (Team/Business plans).
143
     * Uploads the raw source (`blobFormat: 'raw'`); falls back to the
144
     * legacy whole-JSON-body format when options are too large to travel
145
     * inline.
146
     */
147
    private async obfuscateWithBlobUpload(
148
        sourceCode: string,
149
        options: TInputOptions,
150
        legacyRequestBody: string,
151
        onProgress?: TProApiProgressCallback
152
    ): Promise<IProObfuscationResult> {
153
        onProgress?.('Uploading large file...');
30!
154

155
        const followUpSize =
156
            Buffer.byteLength(JSON.stringify({ blobUrl: '', blobFormat: 'raw', options }), 'utf8') +
30✔
157
            ProApiClient.followUpBodyHeadroom;
158
        const useRawFormat = followUpSize <= ProApiClient.blobUploadThreshold;
30✔
159

160
        // `text/plain`: the API rejects executable script MIME types
161
        const blobUrl = useRawFormat
30✔
162
            ? await this.uploadToBlob('obfuscate-source.js', sourceCode, 'text/plain')
30✔
163
            : await this.uploadToBlob('obfuscate-request.json', legacyRequestBody, 'application/json');
164

NEW
165
        const followUpBody = useRawFormat
×
166
            ? JSON.stringify({ blobUrl, blobFormat: 'raw', options })
×
167
            : JSON.stringify({ blobUrl });
168

169
        onProgress?.('File uploaded, starting obfuscation...');
×
170

171
        const headers: Record<string, string> = {
×
172
            // eslint-disable-next-line @typescript-eslint/naming-convention
173
            'Content-Type': 'application/json',
174
            // eslint-disable-next-line @typescript-eslint/naming-convention
175
            'Accept': 'application/x-ndjson',
176
            // eslint-disable-next-line @typescript-eslint/naming-convention
177
            'Authorization': `Bearer ${this.config.apiToken}`
178
        };
179

180
        const controller = new AbortController();
×
181
        const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
×
182

183
        let url = ProApiClient.apiUrl;
×
184

185
        if (this.config.version) {
×
186
            url = `${ProApiClient.apiUrl}?version=${encodeURIComponent(this.config.version)}`;
×
187
        }
188

189
        try {
×
190
            const response = await fetch(url, {
×
191
                method: 'POST',
192
                headers,
193
                body: followUpBody,
194
                signal: controller.signal
195
            });
196

197
            clearTimeout(timeoutId);
×
198

199
            return this.handleStreamingResponse(response, onProgress);
×
200
        } catch (error) {
201
            clearTimeout(timeoutId);
×
202

203
            if (error instanceof Error && error.name === 'AbortError') {
×
204
                throw new ApiError('Request timeout', 408);
×
205
            }
206

207
            throw error;
×
208
        }
209
    }
210

211
    /**
212
     * Upload content to blob storage using client-side upload
213
     */
214
    private async uploadToBlob(pathname: string, content: string, contentType: string): Promise<string> {
215
        // Step 1: Get client upload token from server (server adds random suffix)
216
        const clientToken = await this.getUploadToken(pathname);
30✔
217

218
        // Step 2: Upload directly to Vercel Blob using the client token
219
        return this.uploadWithClientToken(clientToken, pathname, content, contentType);
18✔
220
    }
221

222
    /**
223
     * Get a client upload token from the server
224
     */
225
    private async getUploadToken(pathname: string): Promise<string> {
226
        const controller = new AbortController();
30✔
227
        const timeoutId = setTimeout(() => controller.abort(), 30000);
30✔
228

229
        try {
30✔
230
            const response = await fetch(ProApiClient.uploadTokenUrl, {
30✔
231
                method: 'POST',
232
                headers: {
233
                    // eslint-disable-next-line @typescript-eslint/naming-convention
234
                    'Content-Type': 'application/json',
235
                    // eslint-disable-next-line @typescript-eslint/naming-convention
236
                    'Authorization': `Bearer ${this.config.apiToken}`
237
                },
238
                body: JSON.stringify({ pathname }),
239
                signal: controller.signal
240
            });
241

242
            clearTimeout(timeoutId);
30✔
243

244
            const responseText = await response.text();
30✔
245

246
            let data: { clientToken?: string; error?: string };
247

248
            try {
30✔
249
                data = JSON.parse(responseText);
30✔
250
            } catch {
251
                throw new ApiError(responseText || 'Failed to get upload token', response.status);
×
252
            }
253

254
            if (!response.ok) {
30✔
255
                throw new ApiError(data.error ?? 'Failed to get upload token', response.status);
6!
256
            }
257

258
            if (!data.clientToken) {
24✔
259
                throw new ApiError('No client token returned', 500);
6✔
260
            }
261

262
            return data.clientToken;
18✔
263
        } catch (error) {
264
            clearTimeout(timeoutId);
12✔
265

266
            if (error instanceof ApiError) {
12✔
267
                throw error;
12✔
268
            }
269

270
            if (error instanceof Error && error.name === 'AbortError') {
×
271
                throw new ApiError('Token request timeout', 408);
×
272
            }
273

274
            throw error;
×
275
        }
276
    }
277

278
    /**
279
     * Upload file directly to Vercel Blob using client token
280
     */
281
    private async uploadWithClientToken(
282
        clientToken: string,
283
        pathname: string,
284
        content: string,
285
        contentType: string
286
    ): Promise<string> {
287
        const blobClient = await import('@vercel/blob/client');
36✔
288

289
        const controller = new AbortController();
18✔
290
        const timeoutId = setTimeout(() => controller.abort(), 120000); // 2 minutes for upload
18✔
291

292
        try {
18✔
293
            const blob = await blobClient.put(pathname, content, {
18✔
294
                access: 'public',
295
                token: clientToken,
296
                contentType
297
            });
298

299
            clearTimeout(timeoutId);
×
300

301
            return blob.url;
×
302
        } catch (error) {
303
            clearTimeout(timeoutId);
18✔
304

305
            if (error instanceof ApiError) {
18!
306
                throw error;
×
307
            }
308

309
            if (error instanceof Error && error.name === 'AbortError') {
18!
310
                throw new ApiError('Upload timeout', 408);
×
311
            }
312

313
            if (error instanceof Error) {
18✔
314
                throw new ApiError(`Upload failed: ${error.message}`, 500);
18✔
315
            }
316

317
            throw error;
×
318
        }
319
    }
320

321
    /**
322
     * Handle streaming (NDJSON) response from API
323
     * Supports both direct result and chunked response formats
324
     */
325
    // eslint-disable-next-line complexity
326
    private async handleStreamingResponse(
327
        response: Response,
328
        onProgress?: TProApiProgressCallback
329
    ): Promise<IProObfuscationResult> {
330
        const text = await response.text();
168✔
331
        const lines = text.trim().split('\n');
168✔
332

333
        const messages: IProApiStreamMessage[] = [];
168✔
334

335
        for (const line of lines) {
168✔
336
            if (!line.trim()) {
312!
337
                continue;
×
338
            }
339

340
            try {
312✔
341
                const message: IProApiStreamMessage = JSON.parse(line);
312✔
342
                messages.push(message);
306✔
343

344
                if (message.type === 'progress' && message.message && onProgress) {
306✔
345
                    onProgress(message.message);
30✔
346
                }
347
            } catch {
348
                // Skip invalid JSON lines
349
            }
350
        }
351

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

354
        if (errorMessage) {
168✔
355
            throw new ApiError(errorMessage.message ?? 'Unknown API error', response.status);
12!
356
        }
357

358
        const result = this.reassembleChunkedResponse(messages);
156✔
359

360
        if (!result.code) {
156✔
361
            throw new ApiError('No result received from API', 500);
6✔
362
        }
363

364
        return new ProApiObfuscationResult(result.code, result.sourceMap || '');
150✔
365
    }
366

367
    /**
368
     * Reassemble chunked streaming response
369
     * Handles both chunked format (chunk/chunk_end) and direct result format
370
     */
371
    // eslint-disable-next-line complexity
372
    private reassembleChunkedResponse(messages: IProApiStreamMessage[]): { code: string; sourceMap: string } {
373
        const codeChunks: string[] = [];
156✔
374
        const sourceMapChunks: string[] = [];
156✔
375
        let result = { code: '', sourceMap: '' };
156✔
376

377
        for (const message of messages) {
156✔
378
            switch (message.type) {
288✔
379
                case 'chunk':
222✔
380
                    if (message.field === 'code' && message.data !== undefined && message.index !== undefined) {
72✔
381
                        codeChunks[message.index] = message.data;
60✔
382
                    } else if (
12✔
383
                        message.field === 'sourceMap' &&
36✔
384
                        message.data !== undefined &&
385
                        message.index !== undefined
386
                    ) {
387
                        sourceMapChunks[message.index] = message.data;
12✔
388
                    }
389

390
                    break;
72✔
391

392
                case 'chunk_end':
393
                    result.code = codeChunks.join('');
24✔
394
                    result.sourceMap = (sourceMapChunks.join('') || message.sourceMap) ?? '';
24!
395

396
                    break;
24✔
397

398
                case 'result':
399
                    result = {
126✔
400
                        code: message.code ?? '',
378!
401
                        sourceMap: message.sourceMap ?? ''
378!
402
                    };
403

404
                    break;
126✔
405
            }
406
        }
407

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

© 2026 Coveralls, Inc