Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<ObfuscationResult>`
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "javascript-obfuscator",
"version": "5.0.1",
"version": "5.1.0",
"description": "JavaScript obfuscator",
"keywords": [
"obfuscator",
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/pro-api/IProApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
13 changes: 11 additions & 2 deletions src/pro-api/ProApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}

Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
254 changes: 254 additions & 0 deletions test/unit-tests/pro-api/ProApiClient.spec.ts
Original file line number Diff line number Diff line change
@@ -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...']);
});
});
});
});