diff --git a/.eslintrc.js b/.eslintrc.js index a18e32d6..4f327033 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -271,7 +271,7 @@ module.exports = { 'prefer-const': 'error', 'prefer-object-spread': 'error', 'prefer-template': 'error', - 'quote-props': ['error', 'as-needed'], + 'quote-props': ['off', 'as-needed'], 'quotes': 'off', 'radix': 'error', 'space-before-function-paren': 'off', diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index b5c3667e..6566c026 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -17,7 +17,10 @@ 1. 2. 3. -4. + +## JavaScript Obfuscator Edition +- JavaScript Obfuscator Open Source +- JavaScript Obfuscator Pro via API or [http://obfuscator.io](http://obfuscator.io]) ## Your Environment diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dbedc7c3..e422999f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -24,7 +24,10 @@ assignees: '' 1. 2. 3. -4. + +## JavaScript Obfuscator Edition +- JavaScript Obfuscator Open Source +- JavaScript Obfuscator Pro via API or [http://obfuscator.io](http://obfuscator.io]) ## Your Environment diff --git a/CHANGELOG.md b/CHANGELOG.md index 510fd350..f7f00f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.0.0 +--- +* Add JavaScript Obfuscator PRO support via calling its API + v4.2.1 --- * Downgrade `multimatch` version to avoid esm errors diff --git a/README.md b/README.md index a1dae134..04313a8a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Huge thanks to all supporters! JavaScript Obfuscator is a powerful free obfuscator for JavaScript, containing a variety of features which provide protection for your source code. **Key features:** +- VM obfuscation (via [JavaScript Obfuscator Pro](https://obfuscator.io/)) - variables renaming - strings extraction and encryption - dead code injection @@ -259,6 +260,103 @@ Returns a map object which keys are identifiers of source codes and values are ` Returns an options object for the passed options preset name. +--- + +## :shield: Pro API Methods (VM Obfuscation) + +The Pro API methods provide access to **VM-based bytecode obfuscation** through the [obfuscator.io](https://obfuscator.io) cloud service. VM obfuscation is the most advanced and secure form of code protection available, transforming your JavaScript functions into custom bytecode that runs on an embedded virtual machine. + +**Why VM Obfuscation?** +- **Strongest protection**: Code is converted to bytecode that cannot be directly understood +- **Anti-decompilation**: No standard JavaScript to reverse engineer +- **Customizable VM**: Each obfuscation generates unique opcodes and VM structure +- **Layered security**: Combine with other obfuscation options for defense in depth + +### Getting an API Token + +To use Pro API methods, you need a valid API token from [obfuscator.io](https://obfuscator.io): + +1. Create an account at [obfuscator.io](https://obfuscator.io) +2. Subscribe to a Pro, Team, or Business plan that includes API access +3. Generate your API token at [obfuscator.io/dashboard](https://obfuscator.io/dashboard) + +### `obfuscatePro(sourceCode, options, proApiConfig, onProgress?)` :new: + +**Async method** that obfuscates code using the Pro API with VM-based bytecode obfuscation. + +```javascript +const JavaScriptObfuscator = require('javascript-obfuscator'); + +const result = await JavaScriptObfuscator.obfuscatePro( + `function hello() { console.log("Hello World"); }`, + { + vmObfuscation: true, // Required! + vmObfuscationThreshold: 1, + compact: true + }, + { + apiToken: 'your_javascript_obfuscator_pro_api_token' + } +); + +console.log(result.getObfuscatedCode()); +``` + +**Parameters:** + +* `sourceCode` (`string`) – source code to obfuscate +* `options` (`Object`) – obfuscation options. **Must include `vmObfuscation: true`** +* `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) +* `onProgress` (`function`, optional) – callback for progress updates during obfuscation + +**Returns:** `Promise` + +**Throws:** `ApiError` if: +- `vmObfuscation` is not enabled in options +- API token is invalid or expired +- API request fails + +### Pro API with Progress Updates + +The API uses streaming mode to provide real-time progress updates during obfuscation: + +```javascript +const result = await JavaScriptObfuscator.obfuscatePro( + sourceCode, + { + vmObfuscation: true, + vmObfuscationThreshold: 1 + }, + { + apiToken: 'your_javascript_obfuscator_pro_api_token' + }, + (message) => { + console.log('Progress:', message); + // Output: "Validating request...", "Authenticating...", "Obfuscating...", etc. + } +); +``` + +### Error Handling + +```javascript +const { ApiError } = require('javascript-obfuscator'); + +try { + const result = await JavaScriptObfuscator.obfuscatePro(sourceCode, options, config); +} catch (error) { + if (error instanceof ApiError) { + console.error(`API Error (${error.statusCode}): ${error.message}`); + } else { + throw error; + } +} +``` + +--- + ## CLI usage See [CLI options](#cli-options). @@ -1640,6 +1738,111 @@ The performance will be at a relatively normal level +## JavaScript Obfuscator Pro VM options + +### `vmObfuscation` +Type: `boolean` Default: `false` + +Enables VM-based bytecode obfuscation. When enabled, JavaScript functions are compiled into custom bytecode that runs on an embedded virtual machine. This provides the highest level of protection as the original code logic is completely transformed. + +**Warning:** This significantly increases code size and may impact performance. Use `vmObfuscationThreshold` to control which root-level functions are transformed. + +### `vmObfuscationThreshold` +Type: `number` Default: `1` + +The probability (from 0 to 1) that a function will be transformed to VM bytecode when `vmObfuscation` is enabled. + +- `0` - no functions will be transformed +- `0.5` - 50% of functions will be transformed +- `1` - all functions will be transformed + +### `vmTargetFunctions` +Type: `string[]` Default: `[]` + +Array of root-level function names to target for VM obfuscation. When specified, only these functions will be transformed (subject to `vmObfuscationThreshold`). Empty array means all functions are candidates. + +### `vmExcludeFunctions` +Type: `string[]` Default: `[]` + +Array of root-level function names to exclude from VM obfuscation. These functions will never be transformed regardless of other settings. + +### `vmOpcodeShuffle` +Type: `boolean` Default: `false` + +Randomizes the opcode mapping for each obfuscation run. Makes static analysis more difficult as opcode meanings change between builds. + +### `vmBytecodeEncoding` +Type: `boolean` Default: `false` + +Encodes the bytecode instructions using XOR encryption. The decoding key is derived at runtime, adding another layer of protection. + +### `vmBytecodeArrayEncoding` +Type: `boolean` Default: `false` + +Applies additional encoding to the bytecode array, making it harder to identify bytecode patterns through static analysis. + +### `vmJumpsEncoding` +Type: `boolean` Default: `false` + +Encodes jump targets and offsets in the bytecode. This obscures control flow and makes it harder to follow program execution. + +### `vmDecoyOpcodes` +Type: `boolean` Default: `false` + +Inserts fake opcodes into the dispatcher that are never executed. Increases code complexity and confuses reverse engineering attempts. + +### `vmDeadCodeInjection` +Type: `boolean` Default: `false` + +Injects dead code sequences into the VM bytecode. These sequences are valid but unreachable, adding noise to analysis. + +### `vmSplitDispatcher` +Type: `boolean` Default: `false` + +Splits the VM dispatcher into multiple smaller dispatchers. Makes the execution flow harder to follow. + +### `vmMacroOps` +Type: `boolean` Default: `false` + +Combines common instruction sequences into single macro opcodes. This creates unique instruction patterns that are harder to recognize. + +### `vmDebugProtection` +Type: `boolean` Default: `false` + +Adds anti-debugging measures to the VM runtime. Detects debugger presence and alters behavior when debugging is detected. + +### `vmRuntimeOpcodeDerivation` +Type: `boolean` Default: `false` + +Derives opcode values at runtime through mathematical operations rather than using static values. Makes static analysis significantly harder. + +### `vmStatefulOpcodes` +Type: `boolean` Default: `false` + +Makes opcode interpretation depend on VM state. The same opcode can have different meanings based on execution history. + +### `vmStackEncoding` +Type: `boolean` Default: `false` + +Encodes values pushed to and popped from the VM stack. Adds protection against memory inspection during execution. + +### `vmRandomizeKeys` +Type: `boolean` Default: `false` + +Randomizes encryption keys and other constants used by the VM. Each build produces unique key values. + +### `vmIndirectDispatch` +Type: `boolean` Default: `false` + +Uses indirect function calls for opcode dispatch instead of direct switch/case. Makes control flow analysis more difficult. + +### `vmBytecodeFormat` +Type: `string` Default: `binary` + +Specifies the format used to embed bytecode in the output: +- `binary` - Compact binary representation (smaller size) +- `json` - JSON format (easier debugging, larger size) + ## Frequently Asked Questions ### What javascript versions are supported? diff --git a/index.ts b/index.ts index 5fad2f09..30b32776 100644 --- a/index.ts +++ b/index.ts @@ -6,13 +6,18 @@ import { TObfuscationResultsObject } from './src/types/TObfuscationResultsObject import { TOptionsPreset } from './src/types/options/TOptionsPreset'; import { IObfuscationResult } from './src/interfaces/source-code/IObfuscationResult'; - -import { JavaScriptObfuscator } from './src/JavaScriptObfuscatorFacade'; +import { IProApiConfig, IProObfuscationResult, TProApiProgressCallback } from './src/interfaces/pro-api/IProApiClient'; +import { JavaScriptObfuscator, ApiError } from './src/JavaScriptObfuscatorFacade'; export type ObfuscatorOptions = TInputOptions; export interface ObfuscationResult extends IObfuscationResult {} +export interface ProObfuscationResult extends IProObfuscationResult {} + +export type { IProApiConfig, TProApiProgressCallback }; +export { ApiError }; + /** * @param {string} sourceCode * @param {ObfuscatorOptions} inputOptions @@ -30,6 +35,23 @@ export declare function obfuscateMultiple ; +/** + * Obfuscate code using the Pro API (obfuscator.io) + * Requires a valid API token and vmObfuscation: true + * + * @param {string} sourceCode - Source code to obfuscate + * @param {ObfuscatorOptions} inputOptions - Obfuscation options (must include vmObfuscation: true) + * @param {IProApiConfig} proApiConfig - Pro API configuration including API token + * @param {TProApiProgressCallback} onProgress - Optional callback for progress updates + * @returns {Promise} - Promise resolving to obfuscation result + */ +export declare function obfuscatePro ( + sourceCode: string, + inputOptions: ObfuscatorOptions, + proApiConfig: IProApiConfig, + onProgress?: TProApiProgressCallback +): Promise; + /** * @param {TOptionsPreset} optionsPreset * @returns {TInputOptions} diff --git a/package.json b/package.json index ebdd473f..9d7bc9be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "4.2.1", + "version": "5.0.0", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", @@ -143,5 +143,6 @@ }, "collective": { "url": "https://opencollective.com/javascript-obfuscator" - } + }, + "packageManager": "yarn@1.22.21+sha512.ca75da26c00327d26267ce33536e5790f18ebd53266796fbb664d2a4a5116308042dd8ee7003b276a20eace7d3c5561c3577bdd71bcb67071187af124779620a" } diff --git a/src/JavaScriptObfuscatorFacade.ts b/src/JavaScriptObfuscatorFacade.ts index abc6e260..d5128d0e 100644 --- a/src/JavaScriptObfuscatorFacade.ts +++ b/src/JavaScriptObfuscatorFacade.ts @@ -10,10 +10,13 @@ import { TOptionsPreset } from './types/options/TOptionsPreset'; import { IInversifyContainerFacade } from './interfaces/container/IInversifyContainerFacade'; import { IJavaScriptObfuscator } from './interfaces/IJavaScriptObfsucator'; import { IObfuscationResult } from './interfaces/source-code/IObfuscationResult'; +import { IProApiConfig, IProObfuscationResult, TProApiProgressCallback } from './interfaces/pro-api/IProApiClient'; +import { ApiError } from './pro-api/ApiError'; import { InversifyContainerFacade } from './container/InversifyContainerFacade'; import { Options } from './options/Options'; import { Utils } from './utils/Utils'; +import { ProApiClient } from './pro-api/ProApiClient'; class JavaScriptObfuscatorFacade { /** @@ -87,6 +90,37 @@ class JavaScriptObfuscatorFacade { public static getOptionsByPreset(optionsPreset: TOptionsPreset): TInputOptions { return Options.getOptionsByPreset(optionsPreset); } + + /** + * Obfuscate code using the Pro API (obfuscator.io) + * This method requires a valid API token from obfuscator.io and only works with VM obfuscation. + * + * @param {string} sourceCode - Source code to obfuscate + * @param {TInputOptions} inputOptions - Obfuscation options (must include vmObfuscation: true) + * @param {IProApiConfig} proApiConfig - Pro API configuration including API token + * @param {TProApiProgressCallback} onProgress - Optional callback for progress updates (streaming mode only) + * @returns {Promise} - Promise resolving to obfuscation result + * @throws {ApiError} - If API returns an error or vmObfuscation is not enabled + */ + public static async obfuscatePro( + sourceCode: string, + inputOptions: TInputOptions, + proApiConfig: IProApiConfig, + onProgress?: TProApiProgressCallback + ): Promise { + if (!inputOptions.vmObfuscation) { + throw new ApiError( + 'obfuscatePro method works only with VM obfuscation. Set vmObfuscation: true in options.', + 400 + ); + } + + const client = new ProApiClient(proApiConfig); + + return client.obfuscate(sourceCode, inputOptions, onProgress); + } } export { JavaScriptObfuscatorFacade as JavaScriptObfuscator }; +export { ApiError } from './pro-api/ApiError'; +export type { IProApiConfig, TProApiProgressCallback } from './interfaces/pro-api/IProApiClient'; diff --git a/src/interfaces/pro-api/IProApiClient.ts b/src/interfaces/pro-api/IProApiClient.ts new file mode 100644 index 00000000..50b55ef6 --- /dev/null +++ b/src/interfaces/pro-api/IProApiClient.ts @@ -0,0 +1,85 @@ +import { TIdentifierNamesCache } from '../../types/TIdentifierNamesCache'; + +/** + * Simplified obfuscation result for Pro API responses + * Does not extend IInitializable since results come from the API + */ +export interface IProObfuscationResult { + /** + * @returns {TIdentifierNamesCache} + */ + getIdentifierNamesCache(): TIdentifierNamesCache; + + /** + * @return {string} + */ + getObfuscatedCode(): string; + + /** + * @return {string} + */ + getSourceMap(): string; + + /** + * @return {string} + */ + toString(): string; +} + +/** + * Configuration for the Pro API client + */ +export interface IProApiConfig { + /** + * API token from obfuscator.io + * Get your token at https://obfuscator.io/dashboard + */ + apiToken: string; + + /** + * Request timeout in milliseconds (default: 300000 - 5 minutes) + */ + timeout?: number; +} + +/** + * Progress callback for streaming responses + */ +export type TProApiProgressCallback = (message: string) => void; + +/** + * Streaming message types from the API + * The API always uses streaming mode (NDJSON format) + */ +export interface IProApiStreamMessage { + /** + * Message type: + * - 'progress': Progress update message + * - 'result': Direct result (non-chunked, for small outputs) + * - 'chunk': Chunked data piece (for large outputs) + * - 'chunk_end': End of chunked data + * - 'error': Error message + */ + type: 'progress' | 'result' | 'chunk' | 'chunk_end' | 'error'; + + /** Progress or error message text */ + message?: string; + + /** Obfuscated code (for 'result' type) */ + code?: string; + + /** Source map (for 'result' or 'chunk_end' type) */ + sourceMap?: string; + + /** Field name for chunk: 'code' or 'sourceMap' (for 'chunk' type) */ + field?: 'code' | 'sourceMap'; + + /** Chunk data (for 'chunk' type) */ + data?: string; + + /** Chunk index (for 'chunk' type) */ + index?: number; + + /** Total number of chunks (for 'chunk' type) */ + total?: number; +} diff --git a/src/pro-api/ApiError.ts b/src/pro-api/ApiError.ts new file mode 100644 index 00000000..b4614a3e --- /dev/null +++ b/src/pro-api/ApiError.ts @@ -0,0 +1,14 @@ +/** + * Error thrown by Pro API + */ +export class ApiError extends Error { + public readonly statusCode: number; + public readonly response?: string; + + public constructor(message: string, statusCode: number, response?: string) { + super(message); + this.name = 'ApiError'; + this.statusCode = statusCode; + this.response = response; + } +} diff --git a/src/pro-api/ProApiClient.ts b/src/pro-api/ProApiClient.ts new file mode 100644 index 00000000..2951cda1 --- /dev/null +++ b/src/pro-api/ProApiClient.ts @@ -0,0 +1,183 @@ +import { TInputOptions } from '../types/options/TInputOptions'; +import { + IProApiConfig, + IProApiStreamMessage, + IProObfuscationResult, + TProApiProgressCallback +} from '../interfaces/pro-api/IProApiClient'; +import { ApiError } from './ApiError'; +import { ProApiObfuscationResult } from './ProApiObfuscationResult'; + +/** + * API URL (hardcoded) + */ +const API_URL = 'https://obfuscator.io/api/v1/obfuscate'; + +/** + * Default timeout (5 minutes) + */ +const DEFAULT_TIMEOUT = 300000; + +/** + * Pro API Client + * Handles communication with the obfuscator.io Pro API using streaming mode + */ +export class ProApiClient { + private readonly config: { + apiToken: string; + timeout: number; + }; + + public constructor(config: IProApiConfig) { + this.config = { + apiToken: config.apiToken, + timeout: config.timeout ?? DEFAULT_TIMEOUT + }; + } + + /** + * Obfuscate code using the Pro API (streaming mode) + * @param sourceCode - Source code to obfuscate + * @param options - Obfuscation options + * @param onProgress - Optional progress callback + * @returns Promise resolving to obfuscation result + */ + public async obfuscate( + sourceCode: string, + options: TInputOptions = {}, + onProgress?: TProApiProgressCallback + ): Promise { + // Validate vmObfuscation is enabled + if (!options.vmObfuscation) { + throw new ApiError( + 'obfuscatePro method works only with VM obfuscation. Set vmObfuscation: true in options.', + 400 + ); + } + + // Always use streaming mode + const headers: Record = { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Content-Type': 'application/json', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Accept': 'application/x-ndjson', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Authorization': `Bearer ${this.config.apiToken}` + }; + + const body = JSON.stringify({ + code: sourceCode, + options + }); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); + + try { + const response = await fetch(API_URL, { + method: 'POST', + headers, + body, + signal: controller.signal + }); + + clearTimeout(timeoutId); + + return this.handleStreamingResponse(response, onProgress); + } catch (error) { + clearTimeout(timeoutId); + + if (error instanceof Error && error.name === 'AbortError') { + throw new ApiError('Request timeout', 408); + } + + throw error; + } + } + + /** + * Handle streaming (NDJSON) response from API + * Supports both direct result and chunked response formats + */ + // eslint-disable-next-line complexity + private async handleStreamingResponse( + response: Response, + onProgress?: TProApiProgressCallback + ): Promise { + const text = await response.text(); + const lines = text.trim().split('\n'); + + const messages: IProApiStreamMessage[] = []; + + for (const line of lines) { + if (!line.trim()) { + continue; + } + + try { + const message: IProApiStreamMessage = JSON.parse(line); + messages.push(message); + + // Call progress callback for progress messages + if (message.type === 'progress' && message.message && onProgress) { + onProgress(message.message); + } + } catch { + // Skip invalid JSON lines + } + } + + // Check for error messages + const errorMessage = messages.find((m) => m.type === 'error'); + if (errorMessage) { + throw new ApiError(errorMessage.message ?? 'Unknown API error', response.status); + } + + // Reassemble the result (handles both chunked and non-chunked responses) + const result = this.reassembleChunkedResponse(messages); + + if (!result.code) { + throw new ApiError('No result received from API', 500); + } + + return new ProApiObfuscationResult(result.code, result.sourceMap || ''); + } + + /** + * Reassemble chunked streaming response + * Handles both chunked format (chunk/chunk_end) and direct result format + */ + // eslint-disable-next-line complexity + private reassembleChunkedResponse(messages: IProApiStreamMessage[]): { code: string; sourceMap: string } { + const codeChunks: string[] = []; + const sourceMapChunks: string[] = []; + let result = { code: '', sourceMap: '' }; + + for (const msg of messages) { + switch (msg.type) { + case 'chunk': + if (msg.field === 'code' && msg.data !== undefined && msg.index !== undefined) { + codeChunks[msg.index] = msg.data; + } else if (msg.field === 'sourceMap' && msg.data !== undefined && msg.index !== undefined) { + sourceMapChunks[msg.index] = msg.data; + } + break; + + case 'chunk_end': + result.code = codeChunks.join(''); + result.sourceMap = (sourceMapChunks.join('') || msg.sourceMap) ?? ''; + break; + + case 'result': + // Direct result (non-chunked) + result = { + code: msg.code ?? '', + sourceMap: msg.sourceMap ?? '' + }; + break; + } + } + + return result; + } +} diff --git a/src/pro-api/ProApiObfuscationResult.ts b/src/pro-api/ProApiObfuscationResult.ts new file mode 100644 index 00000000..82bc8962 --- /dev/null +++ b/src/pro-api/ProApiObfuscationResult.ts @@ -0,0 +1,32 @@ +import { TIdentifierNamesCache } from '../types/TIdentifierNamesCache'; +import { IProObfuscationResult } from '../interfaces/pro-api/IProApiClient'; + +/** + * Pro API Obfuscation Result + * Simplified result type for Pro API responses + */ +export class ProApiObfuscationResult implements IProObfuscationResult { + private readonly obfuscatedCode: string; + private readonly sourceMapValue: string; + + public constructor(code: string, sourceMap: string = '') { + this.obfuscatedCode = code; + this.sourceMapValue = sourceMap; + } + + public getObfuscatedCode(): string { + return this.obfuscatedCode; + } + + public getSourceMap(): string { + return this.sourceMapValue; + } + + public getIdentifierNamesCache(): TIdentifierNamesCache { + return null; + } + + public toString(): string { + return this.obfuscatedCode; + } +} diff --git a/test/functional-tests/pro-api/ProApiClient.spec.ts b/test/functional-tests/pro-api/ProApiClient.spec.ts new file mode 100644 index 00000000..1d75bf8e --- /dev/null +++ b/test/functional-tests/pro-api/ProApiClient.spec.ts @@ -0,0 +1,463 @@ +import { assert } from 'chai'; +import * as sinon from 'sinon'; + +import { ProApiClient } from '../../../src/pro-api/ProApiClient'; +import { ApiError } from '../../../src/pro-api/ApiError'; +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +describe('ProApiClient', () => { + let fetchStub: sinon.SinonStub; + + // Helper to create NDJSON streaming response + const createNdjsonResponse = (messages: object[]): string => { + return messages.map((msg) => JSON.stringify(msg)).join('\n'); + }; + + // Mock fetch to redirect to our test server + const mockFetch = (responseBody: string, statusCode: number = 200): void => { + fetchStub = sinon.stub(global, 'fetch').callsFake(async () => { + return { + ok: statusCode >= 200 && statusCode < 300, + status: statusCode, + text: async () => responseBody + } as Response; + }); + }; + + afterEach(() => { + if (fetchStub) { + fetchStub.restore(); + } + }); + + describe('obfuscate', () => { + describe('validation', () => { + it('should throw ApiError when vmObfuscation is not enabled', async () => { + const client = new ProApiClient({ apiToken: 'test-token' }); + + try { + await client.obfuscate('const a = 1;', { vmObfuscation: false }); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.instanceOf(error, ApiError); + assert.include((error as ApiError).message, 'vmObfuscation'); + assert.equal((error as ApiError).statusCode, 400); + } + }); + + it('should throw ApiError when vmObfuscation is undefined', async () => { + const client = new ProApiClient({ apiToken: 'test-token' }); + + try { + await client.obfuscate('const a = 1;', {}); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.instanceOf(error, ApiError); + assert.include((error as ApiError).message, 'vmObfuscation'); + } + }); + }); + + describe('streaming response - direct result', () => { + it('should handle direct result response', async () => { + const obfuscatedCode = 'var _0x1234 = function() { return 1; };'; + const sourceMap = '{"version":3}'; + + const responseBody = createNdjsonResponse([ + { type: 'progress', message: 'Starting obfuscation...' }, + { type: 'progress', message: 'Processing...' }, + { type: 'result', code: obfuscatedCode, sourceMap: sourceMap } + ]); + + mockFetch(responseBody); + + const client = new ProApiClient({ apiToken: 'test-token' }); + const result = await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.equal(result.getObfuscatedCode(), obfuscatedCode); + assert.equal(result.getSourceMap(), sourceMap); + }); + + it('should call progress callback for progress messages', async () => { + const progressMessages: string[] = []; + const obfuscatedCode = 'var _0x1234 = 1;'; + + const responseBody = createNdjsonResponse([ + { type: 'progress', message: 'Step 1: Parsing' }, + { type: 'progress', message: 'Step 2: Transforming' }, + { type: 'progress', message: 'Step 3: Generating' }, + { type: 'result', code: obfuscatedCode, sourceMap: '' } + ]); + + mockFetch(responseBody); + + const client = new ProApiClient({ apiToken: 'test-token' }); + await client.obfuscate('const a = 1;', { vmObfuscation: true }, (msg) => { + progressMessages.push(msg); + }); + + assert.deepEqual(progressMessages, [ + 'Step 1: Parsing', + 'Step 2: Transforming', + 'Step 3: Generating' + ]); + }); + }); + + describe('streaming response - chunked result', () => { + it('should handle chunked code response', async () => { + const chunk1 = 'var _0x1234 = '; + const chunk2 = 'function() { '; + const chunk3 = 'return 1; };'; + const expectedCode = chunk1 + chunk2 + chunk3; + + const responseBody = createNdjsonResponse([ + { type: 'progress', message: 'Processing...' }, + { type: 'chunk', field: 'code', data: chunk1, index: 0, total: 3 }, + { type: 'chunk', field: 'code', data: chunk2, index: 1, total: 3 }, + { type: 'chunk', field: 'code', data: chunk3, index: 2, total: 3 }, + { type: 'chunk_end', sourceMap: '' } + ]); + + mockFetch(responseBody); + + const client = new ProApiClient({ apiToken: 'test-token' }); + const result = await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.equal(result.getObfuscatedCode(), expectedCode); + }); + + it('should handle chunked code and sourceMap response', async () => { + const codeChunk1 = 'var a = 1;'; + const codeChunk2 = 'var b = 2;'; + const mapChunk1 = '{"version":'; + const mapChunk2 = '3}'; + + const responseBody = createNdjsonResponse([ + { type: 'chunk', field: 'code', data: codeChunk1, index: 0, total: 2 }, + { type: 'chunk', field: 'code', data: codeChunk2, index: 1, total: 2 }, + { type: 'chunk', field: 'sourceMap', data: mapChunk1, index: 0, total: 2 }, + { type: 'chunk', field: 'sourceMap', data: mapChunk2, index: 1, total: 2 }, + { type: 'chunk_end' } + ]); + + mockFetch(responseBody); + + const client = new ProApiClient({ apiToken: 'test-token' }); + const result = await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.equal(result.getObfuscatedCode(), codeChunk1 + codeChunk2); + assert.equal(result.getSourceMap(), mapChunk1 + mapChunk2); + }); + + it('should handle out-of-order chunks correctly', async () => { + const chunk0 = 'first'; + const chunk1 = 'second'; + const chunk2 = 'third'; + + const responseBody = createNdjsonResponse([ + { type: 'chunk', field: 'code', data: chunk2, index: 2, total: 3 }, + { type: 'chunk', field: 'code', data: chunk0, index: 0, total: 3 }, + { type: 'chunk', field: 'code', data: chunk1, index: 1, total: 3 }, + { type: 'chunk_end', sourceMap: '' } + ]); + + mockFetch(responseBody); + + const client = new ProApiClient({ apiToken: 'test-token' }); + const result = await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.equal(result.getObfuscatedCode(), chunk0 + chunk1 + chunk2); + }); + }); + + describe('error handling', () => { + it('should throw ApiError when API returns error message', async () => { + const responseBody = createNdjsonResponse([ + { type: 'progress', message: 'Starting...' }, + { type: 'error', message: 'Invalid API token' } + ]); + + mockFetch(responseBody, 401); + + const client = new ProApiClient({ apiToken: 'invalid-token' }); + + try { + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.instanceOf(error, ApiError); + assert.equal((error as ApiError).message, 'Invalid API token'); + } + }); + + it('should throw ApiError when no result is received', async () => { + const responseBody = createNdjsonResponse([ + { type: 'progress', message: 'Processing...' } + ]); + + mockFetch(responseBody); + + const client = new ProApiClient({ apiToken: 'test-token' }); + + try { + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.instanceOf(error, ApiError); + assert.include((error as ApiError).message, 'No result received'); + } + }); + + it('should skip invalid JSON lines in response', async () => { + const obfuscatedCode = 'var a = 1;'; + const responseBody = + '{"type":"progress","message":"Step 1"}\n' + + 'invalid json line\n' + + `{"type":"result","code":"${obfuscatedCode}","sourceMap":""}`; + + mockFetch(responseBody); + + const client = new ProApiClient({ apiToken: 'test-token' }); + const result = await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.equal(result.getObfuscatedCode(), obfuscatedCode); + }); + + it('should handle timeout', async () => { + fetchStub = sinon.stub(global, 'fetch').callsFake(async (url, options) => { + // Simulate abort being called + const signal = options?.signal as AbortSignal; + if (signal) { + const error = new Error('The operation was aborted'); + error.name = 'AbortError'; + throw error; + } + return { ok: true, text: async () => '' } as Response; + }); + + const client = new ProApiClient({ apiToken: 'test-token', timeout: 1 }); + + try { + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.instanceOf(error, ApiError); + assert.equal((error as ApiError).statusCode, 408); + assert.include((error as ApiError).message, 'timeout'); + } + }); + }); + + describe('result interface', () => { + it('should return result implementing IProObfuscationResult', async () => { + const obfuscatedCode = 'var _0x1234 = 1;'; + const sourceMap = '{"version":3}'; + + const responseBody = createNdjsonResponse([ + { type: 'result', code: obfuscatedCode, sourceMap: sourceMap } + ]); + + mockFetch(responseBody); + + const client = new ProApiClient({ apiToken: 'test-token' }); + const result = await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.equal(result.getObfuscatedCode(), obfuscatedCode); + assert.equal(result.getSourceMap(), sourceMap); + assert.isNull(result.getIdentifierNamesCache()); + assert.equal(result.toString(), obfuscatedCode); + }); + }); + }); +}); + +describe('JavaScriptObfuscatorFacade Pro API', () => { + let fetchStub: sinon.SinonStub; + + const createNdjsonResponse = (messages: object[]): string => { + return messages.map((msg) => JSON.stringify(msg)).join('\n'); + }; + + const mockFetch = (responseBody: string, statusCode: number = 200): void => { + fetchStub = sinon.stub(global, 'fetch').callsFake(async () => { + return { + ok: statusCode >= 200 && statusCode < 300, + status: statusCode, + text: async () => responseBody + } as Response; + }); + }; + + afterEach(() => { + if (fetchStub) { + fetchStub.restore(); + } + }); + + describe('obfuscatePro', () => { + it('should throw ApiError when vmObfuscation is not enabled', async () => { + try { + await JavaScriptObfuscator.obfuscatePro('const a = 1;', {}, { apiToken: 'test' }); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.instanceOf(error, ApiError); + assert.include((error as ApiError).message, 'vmObfuscation'); + } + }); + + it('should return obfuscation result on success', async () => { + const obfuscatedCode = 'var _0x1234 = 1;'; + + const responseBody = createNdjsonResponse([ + { type: 'result', code: obfuscatedCode, sourceMap: '' } + ]); + + mockFetch(responseBody); + + const result = await JavaScriptObfuscator.obfuscatePro( + 'const a = 1;', + { vmObfuscation: true }, + { apiToken: 'test-token' } + ); + + assert.equal(result.getObfuscatedCode(), obfuscatedCode); + }); + + it('should forward progress callback', async () => { + const progressMessages: string[] = []; + const obfuscatedCode = 'var a = 1;'; + + const responseBody = createNdjsonResponse([ + { type: 'progress', message: 'Processing...' }, + { type: 'result', code: obfuscatedCode, sourceMap: '' } + ]); + + mockFetch(responseBody); + + await JavaScriptObfuscator.obfuscatePro( + 'const a = 1;', + { vmObfuscation: true }, + { apiToken: 'test-token' }, + (msg) => progressMessages.push(msg) + ); + + assert.deepEqual(progressMessages, ['Processing...']); + }); + }); +}); + +describe('ApiError', () => { + it('should have correct properties', () => { + const error = new ApiError('Test error', 500, '{"error": "details"}'); + + assert.equal(error.message, 'Test error'); + assert.equal(error.statusCode, 500); + assert.equal(error.response, '{"error": "details"}'); + assert.equal(error.name, 'ApiError'); + assert.instanceOf(error, Error); + }); + + it('should work without response parameter', () => { + const error = new ApiError('Test error', 400); + + assert.equal(error.message, 'Test error'); + assert.equal(error.statusCode, 400); + assert.isUndefined(error.response); + }); +}); + +describe('ProApiClient request format', () => { + let fetchStub: sinon.SinonStub; + + afterEach(() => { + if (fetchStub) { + fetchStub.restore(); + } + }); + + it('should send correct headers', async () => { + let capturedHeaders: HeadersInit | undefined; + + fetchStub = sinon.stub(global, 'fetch').callsFake(async (url, options) => { + capturedHeaders = options?.headers; + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ type: 'result', code: 'var a;', sourceMap: '' }) + } as Response; + }); + + const client = new ProApiClient({ apiToken: 'my-api-token' }); + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + const headers = capturedHeaders as Record; + assert.equal(headers['Content-Type'], 'application/json'); + assert.equal(headers['Accept'], 'application/x-ndjson'); + assert.equal(headers['Authorization'], 'Bearer my-api-token'); + }); + + it('should send correct request body', async () => { + let capturedBody: string | undefined; + + fetchStub = sinon.stub(global, 'fetch').callsFake(async (url, options) => { + capturedBody = options?.body as string; + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ type: 'result', code: 'var a;', sourceMap: '' }) + } as Response; + }); + + const client = new ProApiClient({ apiToken: 'test-token' }); + await client.obfuscate('const a = 1;', { + vmObfuscation: true, + compact: true, + stringArray: false + }); + + const body = JSON.parse(capturedBody!); + assert.equal(body.code, 'const a = 1;'); + assert.deepEqual(body.options, { + vmObfuscation: true, + compact: true, + stringArray: false + }); + }); + + it('should use POST method', async () => { + let capturedMethod: string | undefined; + + fetchStub = sinon.stub(global, 'fetch').callsFake(async (url, options) => { + capturedMethod = options?.method; + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ type: 'result', code: 'var a;', sourceMap: '' }) + } as Response; + }); + + const client = new ProApiClient({ apiToken: 'test-token' }); + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.equal(capturedMethod, 'POST'); + }); + + it('should use custom timeout', async () => { + let signalReceived = false; + + fetchStub = sinon.stub(global, 'fetch').callsFake(async (url, options) => { + signalReceived = options?.signal !== undefined; + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ type: 'result', code: 'var a;', sourceMap: '' }) + } as Response; + }); + + const client = new ProApiClient({ apiToken: 'test-token', timeout: 60000 }); + await client.obfuscate('const a = 1;', { vmObfuscation: true }); + + assert.isTrue(signalReceived); + }); +});