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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
Change Log

v5.8.0
---
* Pro API: added support for custom presets. `optionsPreset` accepts the alias of a custom preset saved in the obfuscator.io dashboard; `obfuscatePro()` and the CLI (`--pro-api-token`) fetch it and merge its options
* CLI: `--options-preset` with a VM preset name (e.g. `vm-default`) no longer fails locally when `--pro-api-token` is set; the name is passed to the Pro API

v5.7.0
---
* **New option:** `advertisement` allows to control the display of the JavaScript Obfuscator Pro advertisement message in the console. Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1448
Expand Down
228 changes: 111 additions & 117 deletions README.md

Large diffs are not rendered by default.

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.7.0",
"version": "5.8.0",
"description": "JavaScript obfuscator",
"keywords": [
"obfuscator",
Expand Down
11 changes: 6 additions & 5 deletions src/JavaScriptObfuscatorFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,14 @@ class JavaScriptObfuscatorFacade {

const { ProApiClient } = await import('./pro-api/ProApiClient');

if (!ProApiClient.hasProFeatures(inputOptions)) {
return JavaScriptObfuscatorFacade.obfuscate(sourceCode, inputOptions);
}

const client = new ProApiClient(proApiConfig);
const options: TInputOptions = await client.resolveOptions(inputOptions);

if (!ProApiClient.hasProFeatures(options)) {
return JavaScriptObfuscatorFacade.obfuscate(sourceCode, options);
}

return client.obfuscate(sourceCode, inputOptions, onProgress);
return client.obfuscate(sourceCode, options, onProgress);
}
}

Expand Down
74 changes: 55 additions & 19 deletions src/cli/JavaScriptObfuscatorCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as path from 'path';

import { TInputCLIOptions } from '../types/options/TInputCLIOptions';
import { TInputOptions } from '../types/options/TInputOptions';
import { TOptionsPreset } from '../types/options/TOptionsPreset';

import { IFileData } from '../interfaces/cli/IFileData';
import { IInitializable } from '../interfaces/IInitializable';
Expand Down Expand Up @@ -92,6 +91,11 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
@initializable()
private obfuscatedCodeFileUtils!: ObfuscatedCodeFileUtils;

/**
* @type {ProApiClient | undefined}
*/
private proApiClient?: ProApiClient;

/**
* @type {string[]}
*/
Expand Down Expand Up @@ -121,9 +125,11 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
const configFileLocation: string = configFilePath ? path.resolve(configFilePath, '.') : '';
const configFileOptions: TInputOptions = configFileLocation ? CLIUtils.getUserConfig(configFileLocation) : {};

const presetName: TOptionsPreset =
const presetName: string =
inputCLIOptions.optionsPreset ?? configFileOptions.optionsPreset ?? OptionsPreset.Default;
const presetOptions: TInputOptions = Options.getOptionsByPreset(presetName);
const presetOptions: TInputOptions = Options.isLocalPreset(presetName)
? Options.getOptionsByPreset(presetName)
: { optionsPreset: presetName };

return {
...presetOptions,
Expand Down Expand Up @@ -275,7 +281,9 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
.option(
'--options-preset <string>',
'Allows to set options preset. ' +
`Values: ${CLIUtils.stringifyOptionAvailableValues(OptionsPreset)}. ` +
`Values: ${CLIUtils.stringifyOptionAvailableValues(OptionsPreset)}, ` +
'a Pro VM preset (e.g. vm-default) or the alias of a custom preset saved at obfuscator.io ' +
'(both require --pro-api-token). ' +
`Default: ${OptionsPreset.Default}`
)
.option(
Expand Down Expand Up @@ -665,7 +673,7 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
outputCodePath: string,
sourceCodeIndex: number | null
): Promise<void> {
const options: TInputOptions = {
let options: TInputOptions = {
...this.inputCLIOptions,
identifierNamesCache: this.identifierNamesCacheFileUtils.readFile(),
inputFileName: path.basename(inputCodePath),
Expand All @@ -680,9 +688,20 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
const proApiToken = this.inputCLIOptions.proApiToken;

if (proApiToken && ProApiClient.hasProFeatures(options)) {
await this.processSourceCodeWithProApi(sourceCode, outputCodePath, options, proApiToken);
const client: ProApiClient = this.getProApiClient(proApiToken);

return;
options = await client.resolveOptions(options);

if (ProApiClient.hasProFeatures(options)) {
await this.processSourceCodeWithProApi({
sourceCode: sourceCode,
outputCodePath: outputCodePath,
options: options,
client: client
});

return;
}
}

if (options.sourceMap) {
Expand All @@ -693,21 +712,38 @@ export class JavaScriptObfuscatorCLI implements IInitializable {
}

/**
* Process source code using Pro API (cloud-based VM obfuscation)
* @param {string} apiToken
* @return {ProApiClient}
* @private
*/
private async processSourceCodeWithProApi(
sourceCode: string,
outputCodePath: string,
options: TInputOptions,
apiToken: string
): Promise<void> {
const proApiVersion = this.inputCLIOptions.proApiVersion;
private getProApiClient(apiToken: string): ProApiClient {
if (!this.proApiClient) {
this.proApiClient = new ProApiClient({
apiToken,
version: this.inputCLIOptions.proApiVersion
});
}

const client = new ProApiClient({
apiToken,
version: proApiVersion
});
return this.proApiClient;
}

/**
* @param {{sourceCode: string, outputCodePath: string, options: TInputOptions, client: ProApiClient}} param0
* @private
*/
private async processSourceCodeWithProApi(
{
sourceCode,
outputCodePath,
options,
client
}: {
sourceCode: string;
outputCodePath: string;
options: TInputOptions;
client: ProApiClient;
}
): Promise<void> {
const result: IProObfuscationResult = await client.obfuscate(sourceCode, options, (message: string) => {
Logger.log(Logger.colorInfo, LoggingPrefix.CLI, message);
});
Expand Down
21 changes: 13 additions & 8 deletions src/interfaces/pro-api/IProApiClient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TInputOptions } from '../../types/options/TInputOptions';
import { TIdentifierNamesCache } from '../../types/TIdentifierNamesCache';

/**
Expand Down Expand Up @@ -48,6 +49,18 @@ export interface IProApiConfig {
version?: string;
}

/**
* A custom preset saved in the obfuscator.io dashboard, as returned by
* `GET /api/v1/presets/{alias}`
*/
export interface IProCustomPreset {
alias: string;
name: string;
description: string | null;
options: TInputOptions;
updatedAt: string;
}

/**
* Progress callback for streaming responses
*/
Expand Down Expand Up @@ -89,11 +102,3 @@ export interface IProApiStreamMessage {
/** Total number of chunks (for 'chunk' type) */
total?: number;
}

/**
* Response from the Blob upload endpoint
*/
export interface IProApiBlobUploadResponse {
blobUrl?: string;
error?: string;
}
20 changes: 17 additions & 3 deletions src/options/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,27 @@ export class Options implements IOptions {
* @param {TOptionsPreset} optionsPreset
* @returns {TInputOptions}
*/
public static getOptionsByPreset(optionsPreset: TOptionsPreset): TInputOptions {
const options: TInputOptions | null = Options.optionPresetsMap.get(optionsPreset) ?? null;
public static getOptionsByPreset(optionsPreset: string): TInputOptions {
const options: TInputOptions | null = Options.optionPresetsMap.get(<TOptionsPreset>optionsPreset) ?? null;

if (!options) {
throw new Error(`Options for preset name \`${optionsPreset}\` are not found`);
throw new Error(
`Options for preset name \`${optionsPreset}\` are not found. ` +
'VM presets and custom preset aliases are resolved by the Pro API: ' +
'use `obfuscatePro()` or the `--pro-api-token` CLI option.'
);
}

return options;
}

/**
* Whether a preset name is one this package can expand without the Pro API.
*
* @param {string | undefined} optionsPreset
* @returns {boolean}
*/
public static isLocalPreset(optionsPreset: string | undefined): boolean {
return optionsPreset !== undefined && Options.optionPresetsMap.has(<TOptionsPreset>optionsPreset);
}
}
122 changes: 120 additions & 2 deletions src/pro-api/ProApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { TInputOptions } from '../types/options/TInputOptions';
import {
IProApiConfig,
IProApiStreamMessage,
IProCustomPreset,
IProObfuscationResult,
TProApiProgressCallback
} from '../interfaces/pro-api/IProApiClient';
import { ApiError } from './ApiError';
import { ProApiObfuscationResult } from './ProApiObfuscationResult';
import { ProOptionsPreset } from './enums/ProOptionsPreset';
import { Options } from '../options/Options';

/**
* Pro API Client
Expand All @@ -23,6 +26,10 @@ export class ProApiClient {

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

private static readonly presetsUrl = `${ProApiClient.apiHost}/api/v1/presets`;

private static readonly builtInPresets: ReadonlySet<string> = new Set(Object.values(ProOptionsPreset));

/**
* Default timeout (5 minutes)
*/
Expand All @@ -45,6 +52,8 @@ export class ProApiClient {
version?: string;
};

private readonly presetRequests: Map<string, Promise<IProCustomPreset | null>> = new Map();

public constructor(config: IProApiConfig) {
this.config = {
apiToken: config.apiToken,
Expand All @@ -55,10 +64,65 @@ export class ProApiClient {

/**
* Check if any Pro features are enabled in the options.
* Pro features require the Pro API for cloud-based obfuscation.
* Pro features require the Pro API for cloud-based obfuscation. A preset
* name the local obfuscator cannot expand (a VM preset, or a custom
* preset alias) counts too: only the Pro API can resolve it.
*/
public static hasProFeatures(options: TInputOptions): boolean {
return options.vmObfuscation === true || options.parseHtml === true;
return (
options.vmObfuscation === true ||
options.parseHtml === true ||
(typeof options.optionsPreset === 'string' && !Options.isLocalPreset(options.optionsPreset))
);
}

/**
* Whether an `optionsPreset` value is one the obfuscator itself
* understands. Anything else names a custom preset saved in the dashboard.
*/
public static isBuiltInPreset(optionsPreset: string | undefined): boolean {
return optionsPreset !== undefined && ProApiClient.builtInPresets.has(optionsPreset);
}

/**
* Fetch a custom preset by its alias, or null when the caller has no
* preset with that alias.
* @param alias - The alias set in the dashboard's save dialog
*/
public async fetchPreset(alias: string): Promise<IProCustomPreset | null> {
let request = this.presetRequests.get(alias);

if (!request) {
request = this.requestPreset(alias);
this.presetRequests.set(alias, request);
}

return request;
}

/**
* Options with a custom `optionsPreset` expanded: the preset's saved
* options become the base and the caller's other options override them,
* the same base-then-overrides order the obfuscator applies to a built-in
* preset. A built-in `optionsPreset` (or none) is returned untouched, with
* no request made.
* @param options - Obfuscation options, possibly naming a custom preset
* @throws {ApiError} 404 when the alias names no preset of the caller's
*/
public async resolveOptions(options: TInputOptions): Promise<TInputOptions> {
const { optionsPreset, ...rest } = options;

if (typeof optionsPreset !== 'string' || ProApiClient.isBuiltInPreset(optionsPreset)) {
return options;
}

const preset = await this.fetchPreset(optionsPreset);

if (!preset) {
throw new ApiError(`Custom preset "${optionsPreset}" not found`, 404);
}

return { ...preset.options, ...rest };
}

/**
Expand Down Expand Up @@ -275,6 +339,60 @@ export class ProApiClient {
}
}

/**
* GET /api/v1/presets/{alias}. Same shape as getUploadToken: JSON body,
* `{ error }` on failure, the request timeout mapped to 408.
*/
private async requestPreset(alias: string): Promise<IProCustomPreset | null> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);

try {
const response = await fetch(`${ProApiClient.presetsUrl}/${encodeURIComponent(alias)}`, {
method: 'GET',
headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention
'Authorization': `Bearer ${this.config.apiToken}`
},
signal: controller.signal
});

clearTimeout(timeoutId);

if (response.status === 404) {
return null;
}

const responseText = await response.text();

let data: (IProCustomPreset & { error?: string }) | { error?: string };

try {
data = JSON.parse(responseText);
} catch {
throw new ApiError(responseText || 'Failed to fetch preset', response.status);
}

if (!response.ok) {
throw new ApiError(data.error ?? 'Failed to fetch preset', response.status);
}

return <IProCustomPreset>data;
} catch (error) {
clearTimeout(timeoutId);

if (error instanceof ApiError) {
throw error;
}

if (error instanceof Error && error.name === 'AbortError') {
throw new ApiError('Preset request timeout', 408);
}

throw error;
}
}

/**
* Upload file directly to Vercel Blob using client token
*/
Expand Down
Loading
Loading