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: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Change Log

v5.8.2
v5.9.0
---
* Pro API: added `getProQuota()`, which returns the usage left on the API key's plan.
* Pro API: `ApiError` now carries `code`, the machine-readable cause sent by the API; a build refused for quota has `code` `dailyLimit` or `monthlyLimit`
* The Node `dist` bundle is no longer minified, so uncaught errors print a readable stack frame instead of the whole bundle.

v5.8.1
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,34 @@ The same works from the CLI:
javascript-obfuscator input.js --pro-api-token YOUR_API_TOKEN --options-preset production -o output.js
```

### `getProQuota(proApiConfig)` :new:

**Async method** that returns the usage left on your plan, so a build can check that a set of files fits before it starts instead of failing partway:

```javascript
const quota = await JavaScriptObfuscator.getProQuota({ apiToken: 'YOUR_API_TOKEN' });

// A build counts the UTF-8 size of its source, and at least `minBytesPerBuild`
const cost = files.reduce(
(sum, source) => sum + Math.max(Buffer.byteLength(source, 'utf8'), quota.minBytesPerBuild),
0
);

if (quota.remainingBytes !== null && cost > quota.remainingBytes) {
console.log(`This build needs ${cost} bytes; ${quota.remainingBytes} are left`);
}
```

**Returns:** `Promise<IProQuota>`:

* `remainingBytes` (`number | null`) – bytes you can still use now, the smaller of what is left today and this month; `null` when your plan has no limit
* `minBytesPerBuild` (`number`) – the least a single build counts
* `daily`, `monthly` (`{ usedBytes, limitBytes, resetsAt } | null`) – usage and limit of each window; `resetsAt` is when the window starts over (ISO 8601, UTC); `null` when your plan has no such limit

The size of the output does not count, and a build that ends in an error does not count. On a team, a member's key reports the team's shared monthly usage beside the member's own daily usage, and a team service key reports the team owner's.

**Throws:** `ApiError` with the HTTP `statusCode` if the API returns an error (for example an invalid API token or a plan without API access), or with `statusCode` 408 if the request times out.

### Error Handling

```javascript
Expand All @@ -460,6 +488,23 @@ try {
}
```

A build refused for quota sets `error.code` to `dailyLimit` or `monthlyLimit` (or `usageCheckFailed` when usage could not be checked). Use it with `getProQuota()` to schedule a retry:

```javascript
try {
await JavaScriptObfuscator.obfuscatePro(sourceCode, options, config);
} catch (error) {
if (error instanceof ApiError && (error.code === 'dailyLimit' || error.code === 'monthlyLimit')) {
const quota = await JavaScriptObfuscator.getProQuota(config);
const window = error.code === 'dailyLimit' ? quota.daily : quota.monthly;

scheduleRetry(window.resetsAt);
} else {
throw error;
}
}
```

### CLI Usage with Pro API

You can also use Pro API features directly from the CLI by providing your API token:
Expand Down
16 changes: 14 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import { TObfuscationResultsObject } from './src/types/TObfuscationResultsObject
import { TOptionsPreset } from './src/types/options/TOptionsPreset';

import { IObfuscationResult } from './src/interfaces/source-code/IObfuscationResult';
import { IProApiConfig, IProObfuscationResult, TProApiProgressCallback } from './src/interfaces/pro-api/IProApiClient';
import {
IProApiConfig,
IProObfuscationResult,
IProQuota,
IProQuotaWindow,
TProApiProgressCallback
} from './src/interfaces/pro-api/IProApiClient';
import { JavaScriptObfuscator, ApiError } from './src/JavaScriptObfuscatorFacade';

export type ObfuscatorOptions = TInputOptions;
Expand All @@ -15,7 +21,7 @@ export interface ObfuscationResult extends IObfuscationResult {}

export interface ProObfuscationResult extends IProObfuscationResult {}

export type { IProApiConfig, TProApiProgressCallback };
export type { IProApiConfig, IProQuota, IProQuotaWindow, TProApiProgressCallback };
export { ApiError };

/**
Expand Down Expand Up @@ -52,6 +58,12 @@ export declare function obfuscatePro (
onProgress?: TProApiProgressCallback
): Promise<ProObfuscationResult>;

/**
* @param {IProApiConfig} proApiConfig
* @returns {Promise<IProQuota>}
*/
export declare function getProQuota (proApiConfig: IProApiConfig): Promise<IProQuota>;

/**
* @param {TOptionsPreset} optionsPreset
* @returns {TInputOptions}
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.8.2",
"version": "5.9.0",
"description": "JavaScript obfuscator",
"keywords": [
"obfuscator",
Expand Down
32 changes: 30 additions & 2 deletions src/JavaScriptObfuscatorFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ 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 {
IProApiConfig,
IProObfuscationResult,
IProQuota,
TProApiProgressCallback
} from './interfaces/pro-api/IProApiClient';

import { InversifyContainerFacade } from './container/InversifyContainerFacade';
import { Options } from './options/Options';
Expand Down Expand Up @@ -124,8 +129,31 @@ class JavaScriptObfuscatorFacade {

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

/**
* @param {IProApiConfig} proApiConfig
* @returns {Promise<IProQuota>}
* @throws {ApiError}
*/
public static async getProQuota(proApiConfig: IProApiConfig): Promise<IProQuota> {
if (typeof window !== 'undefined') {
const { ApiError } = await import('./pro-api/ApiError');

throw new ApiError('getProQuota is only available in Node.js environment', 500);
}

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

return new ProApiClient(proApiConfig).getQuota();
}
}

export { JavaScriptObfuscatorFacade as JavaScriptObfuscator };
export { ApiError } from './pro-api/ApiError';
export type { IProApiConfig, IProObfuscationResult, TProApiProgressCallback } from './interfaces/pro-api/IProApiClient';
export type {
IProApiConfig,
IProObfuscationResult,
IProQuota,
IProQuotaWindow,
TProApiProgressCallback
} from './interfaces/pro-api/IProApiClient';
22 changes: 22 additions & 0 deletions src/interfaces/pro-api/IProApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ export interface IProCustomPreset {
updatedAt: string;
}

/**
* One usage window of the API key's plan
*/
export interface IProQuotaWindow {
usedBytes: number;
limitBytes: number;
resetsAt: string;
}

/**
* Usage left on the API key's plan, as returned by `GET /api/v1/quota`
*/
export interface IProQuota {
remainingBytes: number | null;
minBytesPerBuild: number;
daily: IProQuotaWindow | null;
monthly: IProQuotaWindow | null;
}

/**
* Progress callback for streaming responses
*/
Expand All @@ -84,6 +103,9 @@ export interface IProApiStreamMessage {
/** Progress or error message text */
message?: string;

/** Machine-readable cause of an 'error' message, e.g. 'dailyLimit' */
errorCode?: string;

/** Obfuscated code (for 'result' type) */
code?: string;

Expand Down
5 changes: 4 additions & 1 deletion src/pro-api/ApiError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ export class ApiError extends Error {
public readonly statusCode: number;
public readonly response?: string;

public constructor(message: string, statusCode: number, response?: string) {
public readonly code?: string;

public constructor(message: string, statusCode: number, response?: string, code?: string) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.response = response;
this.code = code;
}
}
56 changes: 55 additions & 1 deletion src/pro-api/ProApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
IProApiStreamMessage,
IProCustomPreset,
IProObfuscationResult,
IProQuota,
TProApiProgressCallback
} from '../interfaces/pro-api/IProApiClient';
import { ApiError } from './ApiError';
Expand All @@ -28,6 +29,8 @@ export class ProApiClient {

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

private static readonly quotaUrl = `${ProApiClient.apiHost}/api/v1/quota`;

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

/**
Expand Down Expand Up @@ -100,6 +103,52 @@ export class ProApiClient {
return request;
}

public async getQuota(): Promise<IProQuota> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);

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

clearTimeout(timeoutId);

const responseText = await response.text();
Comment thread
sanex3339 marked this conversation as resolved.
Resolved

let data: { quota?: IProQuota; error?: string };

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

if (!response.ok || !data.quota) {
throw new ApiError(data.error ?? 'Failed to fetch quota', response.status);
}
Comment thread
sanex3339 marked this conversation as resolved.
Resolved

return data.quota;
} catch (error) {
clearTimeout(timeoutId);

if (error instanceof ApiError) {
throw error;
}

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

throw error;
Comment thread
sanex3339 marked this conversation as resolved.
Resolved
}
}

/**
* Options with a custom `optionsPreset` expanded: the preset's saved
* options become the base and the caller's other options override them,
Expand Down Expand Up @@ -470,7 +519,12 @@ export class ProApiClient {
const errorMessage = messages.find((message) => message.type === 'error');

if (errorMessage) {
throw new ApiError(errorMessage.message ?? 'Unknown API error', response.status);
throw new ApiError(
errorMessage.message ?? 'Unknown API error',
response.status,
undefined,
errorMessage.errorCode
);
}

const result = this.reassembleChunkedResponse(messages);
Expand Down
Loading
Loading