From 6866396e9a74dd1d5d8c38f169a40889802907da Mon Sep 17 00:00:00 2001 From: Timofey Kachalov Date: Sat, 13 Dec 2025 18:43:52 +0400 Subject: [PATCH] Add `version` parameter to the `apiConfig` to use different versions of JavaScript Obfuscator Pro via API (#1353) --- CHANGELOG.md | 4 + README.md | 19 ++ package.json | 2 +- src/interfaces/pro-api/IProApiClient.ts | 6 + src/pro-api/ProApiClient.ts | 13 +- test/index.spec.ts | 1 + test/unit-tests/pro-api/ProApiClient.spec.ts | 254 +++++++++++++++++++ 7 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 test/unit-tests/pro-api/ProApiClient.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 359185db..f0f9804b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.1.0 +--- +* Add `version` parameter to the `apiConfig` to use different versions JavaScript Obfuscator Pro via API + v5.0.1 --- * Add JavaScript Obfuscator PRO advertisement message diff --git a/README.md b/README.md index 5c2c4a38..048be086 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,7 @@ console.log(result.getObfuscatedCode()); * `apiConfig` (`Object`) – Pro API configuration: * `apiToken` (`string`, required) – your API token from obfuscator.io * `timeout` (`number`, optional) – request timeout in ms (default: `300000` - 5 minutes) + * `version` (`string`, optional) – JavaScript Obfuscator Pro version to use (e.g., `'5.0.0-beta.20'`). Defaults to latest version if not specified. * `onProgress` (`function`, optional) – callback for progress updates during obfuscation **Returns:** `Promise` @@ -317,6 +318,24 @@ console.log(result.getObfuscatedCode()); - API token is invalid or expired - API request fails +### Pro API with Specific Version + +You can specify which obfuscator version to use via the `version` option: + +```javascript +const result = await JavaScriptObfuscator.obfuscatePro( + sourceCode, + { + vmObfuscation: true, + vmObfuscationThreshold: 1 + }, + { + apiToken: 'your_javascript_obfuscator_pro_api_token', + version: '5.0.0-beta.20' // Use specific version + } +); +``` + ### Pro API with Progress Updates The API uses streaming mode to provide real-time progress updates during obfuscation: diff --git a/package.json b/package.json index 9e91a042..ba8f9d0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.0.1", + "version": "5.1.0", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", diff --git a/src/interfaces/pro-api/IProApiClient.ts b/src/interfaces/pro-api/IProApiClient.ts index 50b55ef6..46712c03 100644 --- a/src/interfaces/pro-api/IProApiClient.ts +++ b/src/interfaces/pro-api/IProApiClient.ts @@ -40,6 +40,12 @@ export interface IProApiConfig { * Request timeout in milliseconds (default: 300000 - 5 minutes) */ timeout?: number; + + /** + * Obfuscator version to use (e.g., '5.0.0-beta.20') + * Defaults to latest version if not specified + */ + version?: string; } /** diff --git a/src/pro-api/ProApiClient.ts b/src/pro-api/ProApiClient.ts index 2951cda1..54edd4d0 100644 --- a/src/pro-api/ProApiClient.ts +++ b/src/pro-api/ProApiClient.ts @@ -26,12 +26,14 @@ export class ProApiClient { private readonly config: { apiToken: string; timeout: number; + version?: string; }; public constructor(config: IProApiConfig) { this.config = { apiToken: config.apiToken, - timeout: config.timeout ?? DEFAULT_TIMEOUT + timeout: config.timeout ?? DEFAULT_TIMEOUT, + version: config.version }; } @@ -73,8 +75,15 @@ export class ProApiClient { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); + // Build URL with optional version parameter + let url = API_URL; + + if (this.config.version) { + url = `${API_URL}?version=${encodeURIComponent(this.config.version)}`; + } + try { - const response = await fetch(API_URL, { + const response = await fetch(url, { method: 'POST', headers, body, diff --git a/test/index.spec.ts b/test/index.spec.ts index f156f832..72d2887b 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -33,6 +33,7 @@ import './unit-tests/node/node-utils/NodeUtils.spec'; import './unit-tests/node/numerical-expression-data-to-node-converter/NumericalExpressionDataToNodeConverter.spec'; import './unit-tests/options/Options.spec'; import './unit-tests/options/ValidationErrorsFormatter.spec'; +import './unit-tests/pro-api/ProApiClient.spec'; import './unit-tests/source-code/ObfuscationResult.spec'; import './unit-tests/source-code/SourceCode.spec'; import './unit-tests/storages/ArrayStorage.spec'; diff --git a/test/unit-tests/pro-api/ProApiClient.spec.ts b/test/unit-tests/pro-api/ProApiClient.spec.ts new file mode 100644 index 00000000..470222ee --- /dev/null +++ b/test/unit-tests/pro-api/ProApiClient.spec.ts @@ -0,0 +1,254 @@ +import 'reflect-metadata'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; + +import { ProApiClient } from '../../../src/pro-api/ProApiClient'; +import { IProApiConfig } from '../../../src/interfaces/pro-api/IProApiClient'; +import { ApiError } from '../../../src/pro-api/ApiError'; + +describe('ProApiClient', () => { + const API_URL = 'https://obfuscator.io/api/v1/obfuscate'; + + let fetchStub: sinon.SinonStub; + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch'); + }); + + afterEach(() => { + fetchStub.restore(); + }); + + describe('constructor', () => { + describe('Variant #1: basic configuration', () => { + it('should create client with required apiToken', () => { + const config: IProApiConfig = { + apiToken: 'test-token' + }; + + const client = new ProApiClient(config); + + assert.isDefined(client); + }); + }); + + describe('Variant #2: configuration with all options', () => { + it('should create client with all configuration options', () => { + const config: IProApiConfig = { + apiToken: 'test-token', + timeout: 60000, + version: '5.0.0-beta.20' + }; + + const client = new ProApiClient(config); + + assert.isDefined(client); + }); + }); + }); + + describe('obfuscate', () => { + describe('Variant #1: vmObfuscation validation', () => { + it('should throw ApiError when vmObfuscation is not enabled', async () => { + const config: IProApiConfig = { + apiToken: 'test-token' + }; + const client = new ProApiClient(config); + + try { + await client.obfuscate('const a = 1;', { compact: true }); + assert.fail('Should have thrown ApiError'); + } catch (error) { + assert.instanceOf(error, ApiError); + assert.include((error as ApiError).message, 'vmObfuscation'); + } + }); + }); + + describe('Variant #2: URL without version parameter', () => { + it('should call API without version query parameter when version is not specified', async () => { + const config: IProApiConfig = { + apiToken: 'test-token' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'result', code: 'obfuscated', sourceMap: '' }), + { status: 200 } + ); + fetchStub.resolves(mockResponse); + + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.isTrue(fetchStub.calledOnce); + const calledUrl = fetchStub.firstCall.args[0]; + assert.strictEqual(calledUrl, API_URL); + }); + }); + + describe('Variant #3: URL with version parameter', () => { + it('should call API with version query parameter when version is specified', async () => { + const config: IProApiConfig = { + apiToken: 'test-token', + version: '5.0.0-beta.20' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'result', code: 'obfuscated', sourceMap: '' }), + { status: 200 } + ); + fetchStub.resolves(mockResponse); + + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.isTrue(fetchStub.calledOnce); + const calledUrl = fetchStub.firstCall.args[0]; + assert.strictEqual(calledUrl, `${API_URL}?version=5.0.0-beta.20`); + }); + }); + + describe('Variant #4: version parameter encoding', () => { + it('should properly encode version parameter in URL', async () => { + const config: IProApiConfig = { + apiToken: 'test-token', + version: '5.0.0-beta.22' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'result', code: 'obfuscated', sourceMap: '' }), + { status: 200 } + ); + fetchStub.resolves(mockResponse); + + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.isTrue(fetchStub.calledOnce); + const calledUrl = fetchStub.firstCall.args[0]; + // encodeURIComponent('5.0.0-beta.22') === '5.0.0-beta.22' (no special chars) + assert.strictEqual(calledUrl, `${API_URL}?version=5.0.0-beta.22`); + }); + }); + + describe('Variant #5: authorization header', () => { + it('should include Authorization header with Bearer token', async () => { + const config: IProApiConfig = { + apiToken: 'my-secret-token', + version: '5.0.0-beta.15' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'result', code: 'obfuscated', sourceMap: '' }), + { status: 200 } + ); + fetchStub.resolves(mockResponse); + + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.isTrue(fetchStub.calledOnce); + const calledOptions = fetchStub.firstCall.args[1]; + assert.strictEqual(calledOptions.headers['Authorization'], 'Bearer my-secret-token'); + }); + }); + + describe('Variant #6: successful obfuscation result', () => { + it('should return obfuscation result with code and sourceMap', async () => { + const config: IProApiConfig = { + apiToken: 'test-token', + version: '5.0.0-beta.20' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'result', code: 'var _0x123=1;', sourceMap: '{"version":3}' }), + { status: 200 } + ); + fetchStub.resolves(mockResponse); + + const result = await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.strictEqual(result.getObfuscatedCode(), 'var _0x123=1;'); + assert.strictEqual(result.getSourceMap(), '{"version":3}'); + }); + }); + + describe('Variant #7: chunked response', () => { + it('should reassemble chunked response correctly', async () => { + const config: IProApiConfig = { + apiToken: 'test-token', + version: '5.0.0-beta.10' + }; + const client = new ProApiClient(config); + + const chunks = [ + JSON.stringify({ type: 'progress', message: 'Processing...' }), + JSON.stringify({ type: 'chunk', field: 'code', data: 'var _0x', index: 0, total: 2 }), + JSON.stringify({ type: 'chunk', field: 'code', data: '123=1;', index: 1, total: 2 }), + JSON.stringify({ type: 'chunk_end', sourceMap: '' }) + ].join('\n'); + + const mockResponse = new Response(chunks, { status: 200 }); + fetchStub.resolves(mockResponse); + + const result = await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.strictEqual(result.getObfuscatedCode(), 'var _0x123=1;'); + }); + }); + + describe('Variant #8: API error response', () => { + it('should throw ApiError when API returns error message', async () => { + const config: IProApiConfig = { + apiToken: 'invalid-token', + version: '5.0.0-beta.20' + }; + const client = new ProApiClient(config); + + const mockResponse = new Response( + JSON.stringify({ type: 'error', message: 'Invalid API token' }), + { status: 401 } + ); + fetchStub.resolves(mockResponse); + + try { + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + assert.fail('Should have thrown ApiError'); + } catch (error) { + assert.instanceOf(error, ApiError); + assert.include((error as ApiError).message, 'Invalid API token'); + } + }); + }); + + describe('Variant #9: progress callback', () => { + it('should call progress callback for progress messages', async () => { + const config: IProApiConfig = { + apiToken: 'test-token', + version: '5.0.0-beta.20' + }; + const client = new ProApiClient(config); + + const progressMessages: string[] = []; + const onProgress = (message: string) => { + progressMessages.push(message); + }; + + const chunks = [ + JSON.stringify({ type: 'progress', message: 'Validating...' }), + JSON.stringify({ type: 'progress', message: 'Obfuscating...' }), + JSON.stringify({ type: 'result', code: 'var a=1;', sourceMap: '' }) + ].join('\n'); + + const mockResponse = new Response(chunks, { status: 200 }); + fetchStub.resolves(mockResponse); + + await client.obfuscate('const a = 1;', { vmObfuscation: true }, onProgress); + + assert.deepEqual(progressMessages, ['Validating...', 'Obfuscating...']); + }); + }); + }); +});