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.5.0
---
* Pro API: reworked large file uploads — fixed `413 Content Too Large` for ~4.4–4.6MB request bodies, and Blob uploads now send the raw source (`blobFormat: 'raw'`) instead of the JSON request body, so uploads always fit the plan's file size cap

v5.4.7
---
* Fixed directory obfuscation with a set `sourceMapFileName` making all files share and overwrite one `.map`. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/817
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.4.7",
"version": "5.5.0",
"description": "JavaScript obfuscator",
"keywords": [
"obfuscator",
Expand Down
58 changes: 42 additions & 16 deletions src/pro-api/ProApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,15 @@ export class ProApiClient {
private static readonly defaultTimeout = 300000;

/**
* Threshold for using blob upload (4.4MB)
* Vercel has a ~4.5MB body limit
* Threshold for using blob upload. Matches the server's inline cap,
* just under Vercel's 4.5MB (decimal) body limit.
*/
private static readonly blobUploadThreshold = 4.4 * 1024 * 1024;
private static readonly blobUploadThreshold = 4_400_000;

/**
* Headroom for the blob URL and JSON envelope in the follow-up request
*/
private static readonly followUpBodyHeadroom = 512;

private readonly config: {
apiToken: string;
Expand Down Expand Up @@ -80,7 +85,7 @@ export class ProApiClient {
const bodySize = Buffer.byteLength(requestBody, 'utf8');

if (bodySize > ProApiClient.blobUploadThreshold) {
return this.obfuscateWithBlobUpload(requestBody, onProgress);
return this.obfuscateWithBlobUpload(sourceCode, options, requestBody, onProgress);
}

return this.obfuscateDirect(requestBody, onProgress);
Expand Down Expand Up @@ -134,15 +139,32 @@ export class ProApiClient {
}

/**
* Obfuscation with blob upload for large files
* Obfuscation with blob upload for large files (Team/Business plans).
* Uploads the raw source (`blobFormat: 'raw'`); falls back to the
* legacy whole-JSON-body format when options are too large to travel
* inline.
*/
private async obfuscateWithBlobUpload(
requestBody: string,
sourceCode: string,
options: TInputOptions,
legacyRequestBody: string,
onProgress?: TProApiProgressCallback
): Promise<IProObfuscationResult> {
onProgress?.('Uploading large file...');

const blobUrl = await this.uploadToBlob(requestBody);
const followUpSize =
Buffer.byteLength(JSON.stringify({ blobUrl: '', blobFormat: 'raw', options }), 'utf8') +
ProApiClient.followUpBodyHeadroom;
const useRawFormat = followUpSize <= ProApiClient.blobUploadThreshold;

// `text/plain`: the API rejects executable script MIME types
const blobUrl = useRawFormat
? await this.uploadToBlob('obfuscate-source.js', sourceCode, 'text/plain')
: await this.uploadToBlob('obfuscate-request.json', legacyRequestBody, 'application/json');

const followUpBody = useRawFormat
? JSON.stringify({ blobUrl, blobFormat: 'raw', options })
: JSON.stringify({ blobUrl });

onProgress?.('File uploaded, starting obfuscation...');

Expand All @@ -168,7 +190,7 @@ export class ProApiClient {
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ blobUrl }),
body: followUpBody,
signal: controller.signal
});

Expand All @@ -187,16 +209,14 @@ export class ProApiClient {
}

/**
* Upload request body to blob storage using client-side upload
* Upload content to blob storage using client-side upload
*/
private async uploadToBlob(requestBody: string): Promise<string> {
const pathname = 'obfuscate-request.json';

private async uploadToBlob(pathname: string, content: string, contentType: string): Promise<string> {
// Step 1: Get client upload token from server (server adds random suffix)
const clientToken = await this.getUploadToken(pathname);

// Step 2: Upload directly to Vercel Blob using the client token
return this.uploadWithClientToken(clientToken, pathname, requestBody);
return this.uploadWithClientToken(clientToken, pathname, content, contentType);
}

/**
Expand Down Expand Up @@ -258,16 +278,22 @@ export class ProApiClient {
/**
* Upload file directly to Vercel Blob using client token
*/
private async uploadWithClientToken(clientToken: string, pathname: string, requestBody: string): Promise<string> {
private async uploadWithClientToken(
clientToken: string,
pathname: string,
content: string,
contentType: string
): Promise<string> {
const blobClient = await import('@vercel/blob/client');

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000); // 2 minutes for upload

try {
const blob = await blobClient.put(pathname, requestBody, {
const blob = await blobClient.put(pathname, content, {
access: 'public',
token: clientToken
token: clientToken,
contentType
});

clearTimeout(timeoutId);
Expand Down
61 changes: 60 additions & 1 deletion test/unit-tests/pro-api/ProApiClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,9 @@ describe('ProApiClient', () => {

describe('Variant #10: client-side blob upload for large files', () => {
const UPLOAD_TOKEN_URL = 'https://obfuscator.io/api/v1/upload/token';
const BLOB_UPLOAD_THRESHOLD = 4.4 * 1024 * 1024;
// Matches the server's inline cap: 4,400,000 bytes, just under
// Vercel's 4.5 MB (decimal) request body limit
const BLOB_UPLOAD_THRESHOLD = 4_400_000;

it('should use direct upload for small files', async () => {
const config: IProApiConfig = {
Expand Down Expand Up @@ -377,6 +379,63 @@ describe('ProApiClient', () => {
assert.include(firstCallOptions.headers['Authorization'], 'Bearer test-token');
});

it('should upload the raw source (not the JSON body) for large code', async () => {
const config: IProApiConfig = {
apiToken: 'test-token'
};
const client = new ProApiClient(config);

const largeCode = 'a'.repeat(BLOB_UPLOAD_THRESHOLD + 1000);

const tokenResponse = new Response(JSON.stringify({ clientToken: 'mock-client-token' }), {
status: 200
});

fetchStub.onFirstCall().resolves(tokenResponse);

try {
await client.obfuscate(largeCode, { vmObfuscation: true });
} catch {
// The blob PUT against the mock token fails; the token
// request that reveals the chosen format has already run.
}

// Raw format uploads the source file itself
const tokenRequestBody = JSON.parse(fetchStub.firstCall.args[1].body);
assert.strictEqual(tokenRequestBody.pathname, 'obfuscate-source.js');
});

it('should fall back to the legacy JSON-body format when options are oversized', async () => {
const config: IProApiConfig = {
apiToken: 'test-token'
};
const client = new ProApiClient(config);

// Small code, but options too large to travel inline in the
// raw-format follow-up request
const oversizedOptions = {
vmObfuscation: true,
reservedStrings: ['x'.repeat(BLOB_UPLOAD_THRESHOLD + 1000)]
};

const tokenResponse = new Response(JSON.stringify({ clientToken: 'mock-client-token' }), {
status: 200
});

fetchStub.onFirstCall().resolves(tokenResponse);

try {
await client.obfuscate('const a = 1;', oversizedOptions);
} catch {
// The blob PUT against the mock token fails; the token
// request that reveals the chosen format has already run.
}

// Legacy format uploads the whole JSON request body
const tokenRequestBody = JSON.parse(fetchStub.firstCall.args[1].body);
assert.strictEqual(tokenRequestBody.pathname, 'obfuscate-request.json');
});

it('should throw ApiError when token request fails', async () => {
const config: IProApiConfig = {
apiToken: 'test-token'
Expand Down
Loading