diff --git a/CHANGELOG.md b/CHANGELOG.md index 9837cce85..b173fb420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index fcd4b84fc..c6c70fff4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.4.7", + "version": "5.5.0", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", diff --git a/src/pro-api/ProApiClient.ts b/src/pro-api/ProApiClient.ts index 1c9c859b1..1da6f7eda 100644 --- a/src/pro-api/ProApiClient.ts +++ b/src/pro-api/ProApiClient.ts @@ -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; @@ -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); @@ -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 { 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...'); @@ -168,7 +190,7 @@ export class ProApiClient { const response = await fetch(url, { method: 'POST', headers, - body: JSON.stringify({ blobUrl }), + body: followUpBody, signal: controller.signal }); @@ -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 { - const pathname = 'obfuscate-request.json'; - + private async uploadToBlob(pathname: string, content: string, contentType: string): Promise { // 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); } /** @@ -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 { + private async uploadWithClientToken( + clientToken: string, + pathname: string, + content: string, + contentType: string + ): Promise { 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); diff --git a/test/unit-tests/pro-api/ProApiClient.spec.ts b/test/unit-tests/pro-api/ProApiClient.spec.ts index b898a10f3..c99d68edf 100644 --- a/test/unit-tests/pro-api/ProApiClient.spec.ts +++ b/test/unit-tests/pro-api/ProApiClient.spec.ts @@ -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 = { @@ -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'